orinium_browser/engine/layouter/
processor.rs1use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::Mutex;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use ui_layout::LayoutNode;
22
23use super::builder::{InheritedCss, build_layout_and_info_from_snapshot};
24use super::css_resolver::{MediaEnvironment, ResolvedStyles};
25use super::dom_snapshot::{DomSnapshot, NodeId};
26use super::match_cache::SelectorMatchCache;
27use super::types::InfoNode;
28use crate::engine::background_worker::BackgroundWorker;
29use crate::engine::bridge::audio::AudioSinkFactory;
30use crate::engine::bridge::text::TextMeasurer;
31use crate::engine::css::matcher::ElementChain;
32use crate::engine::html::ScriptingMode;
33use crate::engine::layouter::css_resolver::RuleSet;
34use crate::engine::layouter::types::ColorScheme;
35use crate::engine::layouter::types::InteractionState;
36use crate::engine::renderer_model::Image;
37use crate::engine::ui::custom_node::CustomNode;
38use crate::engine::ui::registry::DomWriteBack;
39use crate::{perf_scope, profile_log};
40
41pub struct LayoutTask {
43 pub snapshot: Arc<DomSnapshot>,
44 pub root: NodeId,
45 pub resolved_styles: Arc<ResolvedStyles>,
46 pub media_environment: MediaEnvironment,
47 pub measurer: Arc<dyn TextMeasurer>,
48 pub audio_sinks: Arc<dyn AudioSinkFactory>,
49 pub system_color_scheme: ColorScheme,
50 pub scripting_mode: ScriptingMode,
51 pub images: HashMap<String, Image>,
52 pub audio: HashMap<String, Arc<[u8]>>,
53 pub parent: InheritedCss,
54 pub chain: ElementChain,
55 pub write_back_sender: Option<DomWriteBack>,
56 pub styles_version: u64,
60 pub dom_version: u64,
63 pub version: u64,
66 pub interaction: Arc<InteractionState>,
68 pub interaction_version: u64,
70 pub existing_custom_nodes: HashMap<u32, Arc<dyn CustomNode>>,
73 pub animated_overrides: HashMap<NodeId, HashMap<String, crate::engine::css::values::CssValue>>,
75}
76
77struct WorkerCache {
84 styles_version: u64,
86 media_environment: MediaEnvironment,
88 rule_set: RuleSet,
90 have_rule_set: bool,
92 match_cache: SelectorMatchCache,
94 match_cache_dom_version: u64,
96 match_cache_interaction_version: u64,
98}
99
100impl Default for WorkerCache {
101 fn default() -> Self {
102 Self {
103 styles_version: 0,
104 media_environment: MediaEnvironment::new((0.0, 0.0), ColorScheme::Light),
105 rule_set: RuleSet::default(),
106 have_rule_set: false,
107 match_cache: SelectorMatchCache::new(),
108 match_cache_dom_version: 0,
109 match_cache_interaction_version: 0,
110 }
111 }
112}
113
114pub struct LayoutResult {
116 pub layout: LayoutNode,
117 pub info: InfoNode,
118 pub animation_inputs: crate::engine::animation::AnimationInputs,
120 pub version: u64,
122}
123
124enum LayoutCommand {
125 Build(LayoutTask),
126}
127
128struct SendableResult(*mut LayoutResult);
151
152unsafe impl Send for SendableResult {}
155
156pub struct LayoutProcessor {
159 worker: BackgroundWorker<LayoutCommand, Option<SendableResult>>,
160 latest: Arc<AtomicU64>,
163}
164
165impl std::fmt::Debug for LayoutProcessor {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.write_str("LayoutProcessor")
168 }
169}
170
171impl Default for LayoutProcessor {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl LayoutProcessor {
178 pub fn new() -> Self {
179 let latest = Arc::new(AtomicU64::new(0));
180 let latest_clone = Arc::clone(&latest);
181 let cache = Arc::new(Mutex::new(WorkerCache::default()));
182
183 let worker = BackgroundWorker::new(1, move |cmd: LayoutCommand| {
184 match cmd {
185 LayoutCommand::Build(task) => {
186 if task.version < latest_clone.load(Ordering::SeqCst) {
190 return None;
191 }
192 let _version = task.version;
193 perf_scope!(worker_total);
194
195 let mut cache = cache.lock().expect("layout worker cache poisoned");
196 let cache_hit = cache.have_rule_set
197 && cache.styles_version == task.styles_version
198 && cache.media_environment == task.media_environment;
199
200 #[cfg(any(feature = "profile", debug_assertions))]
201 let ruleset_build_time;
202 if cache_hit {
203 #[cfg(any(feature = "profile", debug_assertions))]
204 {
205 ruleset_build_time = std::time::Duration::ZERO;
206 }
207 } else {
208 perf_scope!(ruleset_build);
210 cache.rule_set = RuleSet::from_declarations(
211 &task.resolved_styles,
212 &task.media_environment,
213 );
214 #[cfg(any(feature = "profile", debug_assertions))]
215 {
216 ruleset_build_time = ruleset_build.elapsed();
217 }
218 cache.styles_version = task.styles_version;
219 cache.media_environment = task.media_environment;
220 cache.have_rule_set = true;
221 cache.match_cache.clear();
224 }
225
226 if cache.match_cache_dom_version != task.dom_version
231 || cache.match_cache_interaction_version != task.interaction_version
232 {
233 cache.match_cache.clear();
234 cache.match_cache_dom_version = task.dom_version;
235 cache.match_cache_interaction_version = task.interaction_version;
236 }
237
238 let WorkerCache {
239 rule_set,
240 match_cache,
241 ..
242 } = &mut *cache;
243 let (layout, info, animation_inputs) = build_layout_and_info_from_snapshot(
244 &task.snapshot,
245 task.root,
246 rule_set,
247 task.measurer,
248 task.audio_sinks,
249 task.parent,
250 task.chain,
251 task.system_color_scheme,
252 task.scripting_mode,
253 &task.images,
254 &task.audio,
255 task.write_back_sender,
256 Some(match_cache),
257 &task.interaction,
258 &task.existing_custom_nodes,
259 &task.animated_overrides,
260 );
261 let result = LayoutResult {
262 layout,
263 info,
264 animation_inputs,
265 version: task.version,
266 };
267
268 profile_log!(
269 target: "LayoutRun",
270 log::Level::Info,
271 "[LayoutRun] build: total {:?} | ruleset_build: {:?} | cache_hit: {} | {} | selector_match_cache: {}/{} (skips {})",
272 worker_total.elapsed(),
273 ruleset_build_time,
274 cache_hit,
275 cache.match_cache.stats(),
276 cache.match_cache.len(),
277 cache.match_cache.capacity(),
278 cache.match_cache.stats().skips,
279 );
280 Some(SendableResult(Box::into_raw(Box::new(result))))
281 }
282 }
283 });
284
285 Self { worker, latest }
286 }
287
288 pub fn send(&self, task: LayoutTask) -> u64 {
293 let mut task = task;
294 task.version = self.latest.fetch_add(1, Ordering::SeqCst) + 1;
295 let version = task.version;
296 self.worker.send(LayoutCommand::Build(task));
297 version
298 }
299
300 pub fn try_receive(&self) -> Option<LayoutResult> {
302 let inner = self.worker.try_receive()?;
303 let SendableResult(ptr) = inner?;
304 let boxed = unsafe { Box::from_raw(ptr) };
307 Some(*boxed)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use std::sync::mpsc;
314 use std::time::{Duration, Instant};
315
316 use super::*;
317 use crate::engine::bridge::audio::NullAudioSinkFactory;
318 use crate::engine::bridge::text::FallbackTextMeasurer;
319 use crate::engine::html::parser::Parser as HtmlParser;
320 use crate::engine::layouter::types::NodeKind;
321 use crate::engine::ui::custom_node::CustomNode;
322 use crate::engine::ui::input_text_types::InputTextEvent;
323
324 fn sample_task(write_back_sender: Option<DomWriteBack>) -> LayoutTask {
325 let html = "<html><body><p>hello</p><input value='a'></body></html>";
326 let dom = HtmlParser::new(html).parse();
327 let (snapshot, _dom_refs) = DomSnapshot::from_tree(&dom.root);
328 let root = snapshot.roots()[0];
329 LayoutTask {
330 snapshot: Arc::new(snapshot),
331 root,
332 resolved_styles: Arc::new(ResolvedStyles::default()),
333 media_environment: MediaEnvironment::new((0.0, 0.0), ColorScheme::Light),
334 measurer: Arc::new(FallbackTextMeasurer),
335 audio_sinks: Arc::new(NullAudioSinkFactory),
336 system_color_scheme: ColorScheme::Light,
337 scripting_mode: ScriptingMode::default(),
338 images: HashMap::new(),
339 audio: HashMap::new(),
340 parent: InheritedCss::default(),
341 chain: ElementChain::default(),
342 write_back_sender,
343 styles_version: 0,
344 dom_version: 0,
345 version: 0,
346 interaction: Arc::new(InteractionState::default()),
347 interaction_version: 0,
348 existing_custom_nodes: HashMap::new(),
349 animated_overrides: HashMap::new(),
350 }
351 }
352
353 fn wait_for_result(processor: &LayoutProcessor) -> LayoutResult {
354 let deadline = Instant::now() + Duration::from_secs(5);
355 loop {
356 if let Some(result) = processor.try_receive() {
357 return result;
358 }
359 assert!(
360 Instant::now() < deadline,
361 "layout result did not arrive before the timeout"
362 );
363 std::thread::sleep(Duration::from_millis(1));
364 }
365 }
366
367 fn find_custom<'i>(info: &'i InfoNode, role: &str) -> Option<&'i dyn CustomNode> {
368 if let NodeKind::Custom { node, .. } = &info.kind
369 && node.role() == Some(role)
370 {
371 return Some(&**node);
372 }
373 info.children.iter().find_map(|c| find_custom(c, role))
374 }
375
376 #[test]
377 fn layout_task_round_trips_through_background_thread() {
378 let processor = LayoutProcessor::new();
379 let requested_version = processor.send(sample_task(None));
380
381 let result = wait_for_result(&processor);
382 assert_eq!(result.version, requested_version);
383 assert!(
384 !result.info.children.is_empty(),
385 "the built layout must not be empty"
386 );
387 }
388
389 #[test]
390 fn input_edits_are_reported_through_write_back_channel() {
391 let (tx, rx) = mpsc::channel::<(u32, crate::engine::ui::registry::DomWrite)>();
392 let processor = LayoutProcessor::new();
393 processor.send(sample_task(Some(tx)));
394
395 let result = wait_for_result(&processor);
396 let input = find_custom(&result.info, "textbox")
397 .expect("input component must exist in the Info tree");
398
399 input.handle_text_input(InputTextEvent::Insert("hello".into()));
400
401 let (node_id, write) = rx
402 .recv_timeout(Duration::from_secs(5))
403 .expect("write-back was never sent");
404 assert!(node_id > 0, "input node id is invalid");
405 assert_eq!(
406 write,
407 crate::engine::ui::registry::DomWrite::Value("ahello".into())
408 );
409 }
410}