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::types::InfoNode;
27use crate::engine::background_worker::BackgroundWorker;
28use crate::engine::bridge::text::TextMeasurer;
29use crate::engine::css::matcher::ElementChain;
30use crate::engine::html::ScriptingMode;
31use crate::engine::layouter::css_resolver::RuleSet;
32use crate::engine::layouter::types::ColorScheme;
33use crate::engine::renderer_model::Image;
34use crate::engine::ui::registry::DomWriteBack;
35use crate::{perf_scope, profile_log};
36
37pub struct LayoutTask {
39 pub snapshot: Arc<DomSnapshot>,
40 pub root: NodeId,
41 pub resolved_styles: Arc<ResolvedStyles>,
42 pub media_environment: MediaEnvironment,
43 pub measurer: Arc<dyn TextMeasurer>,
44 pub system_color_scheme: ColorScheme,
45 pub scripting_mode: ScriptingMode,
46 pub images: HashMap<String, Image>,
47 pub audio: HashMap<String, Arc<[u8]>>,
48 pub parent: InheritedCss,
49 pub chain: ElementChain,
50 pub write_back_sender: Option<DomWriteBack>,
51 pub styles_version: u64,
55 pub version: u64,
58}
59
60struct RuleSetCache {
70 styles_version: u64,
72 media_environment: MediaEnvironment,
74 rule_set: RuleSet,
76 have_rule_set: bool,
78}
79
80impl Default for RuleSetCache {
81 fn default() -> Self {
82 Self {
83 styles_version: 0,
84 media_environment: MediaEnvironment::new((0.0, 0.0), ColorScheme::Light),
85 rule_set: RuleSet::default(),
86 have_rule_set: false,
87 }
88 }
89}
90
91pub struct LayoutResult {
93 pub layout: LayoutNode,
94 pub info: InfoNode,
95 pub version: u64,
97}
98
99enum LayoutCommand {
100 Build(LayoutTask),
101}
102
103struct SendableResult(*mut LayoutResult);
117
118unsafe impl Send for SendableResult {}
121
122pub struct LayoutProcessor {
125 worker: BackgroundWorker<LayoutCommand, Option<SendableResult>>,
126 latest: Arc<AtomicU64>,
129}
130
131impl std::fmt::Debug for LayoutProcessor {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.write_str("LayoutProcessor")
134 }
135}
136
137impl Default for LayoutProcessor {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl LayoutProcessor {
144 pub fn new() -> Self {
145 let latest = Arc::new(AtomicU64::new(0));
146 let latest_clone = Arc::clone(&latest);
147 let cache = Arc::new(Mutex::new(RuleSetCache::default()));
148
149 let worker = BackgroundWorker::new(1, move |cmd: LayoutCommand| {
150 match cmd {
151 LayoutCommand::Build(task) => {
152 if task.version < latest_clone.load(Ordering::SeqCst) {
156 return None;
157 }
158 let version = task.version;
159 perf_scope!(worker_total);
160
161 let mut cache = cache.lock().expect("layout rule-set cache poisoned");
162 let cache_hit = cache.have_rule_set
163 && cache.styles_version == task.styles_version
164 && cache.media_environment == task.media_environment;
165
166 #[cfg(any(feature = "profile", debug_assertions))]
167 let ruleset_build_time;
168 if cache_hit {
169 #[cfg(any(feature = "profile", debug_assertions))]
170 {
171 ruleset_build_time = std::time::Duration::ZERO;
172 }
173 } else {
174 perf_scope!(ruleset_build);
176 cache.rule_set = RuleSet::from_declarations(
177 &task.resolved_styles,
178 &task.media_environment,
179 );
180 #[cfg(any(feature = "profile", debug_assertions))]
181 {
182 ruleset_build_time = ruleset_build.elapsed();
183 }
184 cache.styles_version = task.styles_version;
185 cache.media_environment = task.media_environment;
186 cache.have_rule_set = true;
187 }
188
189 let (layout, info) = build_layout_and_info_from_snapshot(
190 &task.snapshot,
191 task.root,
192 &cache.rule_set,
193 task.measurer,
194 task.parent,
195 task.chain,
196 task.system_color_scheme,
197 task.scripting_mode,
198 &task.images,
199 &task.audio,
200 task.write_back_sender,
201 );
202 let result = LayoutResult {
203 layout,
204 info,
205 version,
206 };
207
208 profile_log!(
209 target: "LayoutRun",
210 log::Level::Info,
211 "[LayoutRun] build: total {:?} | ruleset_build: {:?} | cache_hit: {}",
212 worker_total.elapsed(),
213 ruleset_build_time,
214 cache_hit,
215 );
216 Some(SendableResult(Box::into_raw(Box::new(result))))
217 }
218 }
219 });
220
221 Self { worker, latest }
222 }
223
224 pub fn send(&self, task: LayoutTask) -> u64 {
229 let mut task = task;
230 task.version = self.latest.fetch_add(1, Ordering::SeqCst) + 1;
231 let version = task.version;
232 self.worker.send(LayoutCommand::Build(task));
233 version
234 }
235
236 pub fn try_receive(&self) -> Option<LayoutResult> {
238 let inner = self.worker.try_receive()?;
239 let SendableResult(ptr) = inner?;
240 let boxed = unsafe { Box::from_raw(ptr) };
243 Some(*boxed)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use std::sync::mpsc;
250 use std::time::{Duration, Instant};
251
252 use super::*;
253 use crate::engine::bridge::text::FallbackTextMeasurer;
254 use crate::engine::html::parser::Parser as HtmlParser;
255 use crate::engine::layouter::types::NodeKind;
256 use crate::engine::ui::custom_node::CustomNode;
257 use crate::engine::ui::input_text_types::InputTextEvent;
258
259 fn sample_task(write_back_sender: Option<DomWriteBack>) -> LayoutTask {
260 let html = "<html><body><p>hello</p><input value='a'></body></html>";
261 let dom = HtmlParser::new(html).parse();
262 let (snapshot, _dom_refs) = DomSnapshot::from_tree(&dom.root);
263 let root = snapshot.roots()[0];
264 LayoutTask {
265 snapshot: Arc::new(snapshot),
266 root,
267 resolved_styles: Arc::new(ResolvedStyles::default()),
268 media_environment: MediaEnvironment::new((0.0, 0.0), ColorScheme::Light),
269 measurer: Arc::new(FallbackTextMeasurer),
270 system_color_scheme: ColorScheme::Light,
271 scripting_mode: ScriptingMode::default(),
272 images: HashMap::new(),
273 audio: HashMap::new(),
274 parent: InheritedCss::default(),
275 chain: ElementChain::default(),
276 write_back_sender,
277 styles_version: 0,
278 version: 0,
279 }
280 }
281
282 fn wait_for_result(processor: &LayoutProcessor) -> LayoutResult {
283 let deadline = Instant::now() + Duration::from_secs(5);
284 loop {
285 if let Some(result) = processor.try_receive() {
286 return result;
287 }
288 assert!(
289 Instant::now() < deadline,
290 "layout result did not arrive before the timeout"
291 );
292 std::thread::sleep(Duration::from_millis(1));
293 }
294 }
295
296 fn find_custom<'i>(info: &'i InfoNode, role: &str) -> Option<&'i dyn CustomNode> {
297 if let NodeKind::Custom { node, .. } = &info.kind
298 && node.role() == Some(role)
299 {
300 return Some(&**node);
301 }
302 info.children.iter().find_map(|c| find_custom(c, role))
303 }
304
305 #[test]
306 fn layout_task_round_trips_through_background_thread() {
307 let processor = LayoutProcessor::new();
308 let requested_version = processor.send(sample_task(None));
309
310 let result = wait_for_result(&processor);
311 assert_eq!(result.version, requested_version);
312 assert!(
313 !result.info.children.is_empty(),
314 "the built layout must not be empty"
315 );
316 }
317
318 #[test]
319 fn input_edits_are_reported_through_write_back_channel() {
320 let (tx, rx) = mpsc::channel::<(u32, String)>();
321 let processor = LayoutProcessor::new();
322 processor.send(sample_task(Some(tx)));
323
324 let result = wait_for_result(&processor);
325 let input = find_custom(&result.info, "textbox")
326 .expect("input component must exist in the Info tree");
327
328 input.handle_text_input(InputTextEvent::Insert("hello".into()));
329
330 let (node_id, value) = rx
331 .recv_timeout(Duration::from_secs(5))
332 .expect("write-back was never sent");
333 assert!(node_id > 0, "input node id is invalid");
334 assert_eq!(value, "ahello");
335 }
336}