1use std::sync::Arc;
10
11use super::event_handler::{EventHandler, InputContext};
12use super::viewport::Viewport;
13use crate::compositor::layer_builder::LayerTreeBuilder;
14use crate::compositor::layer_tree::LayerTree;
15use crate::dirty_phases::DirtyPhases;
16use crate::dom::document::Document;
17use crate::events::ui_event::ScrollEvent;
18use crate::input::InputEvent;
19use crate::input::default_action::DefaultAction;
20use crate::layout::context::LayoutContext;
21use crate::layout::fragment::Fragment;
22use crate::layout::hit_test::HitTester;
23use crate::layout::inline::FontSystem;
24use crate::lifecycle::LifecycleState;
25use crate::paint::DisplayList;
26use crate::paint::Painter;
27use crate::scroll::{ScrollController, ScrollOffsets, ScrollTree};
28
29pub struct FrameWidget {
66 doc: Document,
67 viewport: Viewport,
68 event_handler: EventHandler,
69
70 lifecycle: LifecycleState,
73
74 dirty: DirtyPhases,
76
77 last_fragment: Option<Arc<Fragment>>,
79
80 font_system: FontSystem,
83
84 last_display_list: Option<Arc<DisplayList>>,
87
88 painted_fragment: Option<Arc<Fragment>>,
90
91 needs_full_layout: bool,
93
94 last_timing: kozan_primitives::timing::FrameTiming,
96
97 last_layer_tree: Option<LayerTree>,
100
101 scroll_tree: ScrollTree,
104
105 scroll_offsets: ScrollOffsets,
108}
109
110impl FrameWidget {
111 #[must_use]
112 pub fn new() -> Self {
113 let mut doc = Document::new();
114 doc.init_body();
115
116 Self {
117 doc,
118 viewport: Viewport::default(),
119 event_handler: EventHandler::new(),
120 lifecycle: LifecycleState::default(),
121 dirty: DirtyPhases::default(),
122 last_fragment: None,
123 font_system: FontSystem::new(),
124 last_display_list: None,
125 painted_fragment: None,
126 needs_full_layout: false,
127 last_timing: kozan_primitives::timing::FrameTiming::default(),
128 last_layer_tree: None,
129 scroll_tree: ScrollTree::new(),
130 scroll_offsets: ScrollOffsets::new(),
131 }
132 }
133
134 #[inline]
135 pub fn document(&self) -> &Document {
136 &self.doc
137 }
138
139 #[inline]
140 pub fn document_mut(&mut self) -> &mut Document {
141 &mut self.doc
142 }
143
144 #[inline]
145 pub fn font_system(&self) -> &FontSystem {
146 &self.font_system
147 }
148
149 #[inline]
150 pub fn viewport(&self) -> &Viewport {
151 &self.viewport
152 }
153
154 #[inline]
155 pub fn last_fragment(&self) -> Option<&Arc<Fragment>> {
156 self.last_fragment.as_ref()
157 }
158
159 #[inline]
160 pub fn lifecycle(&self) -> LifecycleState {
161 self.lifecycle
162 }
163
164 pub fn handle_input(&mut self, event: InputEvent) -> bool {
172 let Some(fragment) = &self.last_fragment else {
173 return false;
174 };
175 let fragment = Arc::clone(fragment);
176
177 let hit_tester = HitTester::new(&self.scroll_offsets);
178 let viewport_height = self.viewport.logical_height() as f32;
179 let ctx = InputContext {
180 surface: &self.doc,
181 fragment: &fragment,
182 hit_tester: &hit_tester,
183 viewport_height,
184 scroll_tree: &self.scroll_tree,
185 };
186
187 let result = self.event_handler.handle_input(event, &ctx);
188
189 if result.state_changed {
190 self.dirty.invalidate_style();
191 }
192
193 match result.default_action {
194 DefaultAction::Scroll { target, delta } => {
195 self.apply_scroll(target, delta) || result.state_changed
196 }
197 DefaultAction::FocusNext | DefaultAction::FocusPrev | DefaultAction::Activate => {
198 result.state_changed
200 }
201 DefaultAction::ScrollPrevented | DefaultAction::None => result.state_changed,
202 }
203 }
204
205 fn apply_scroll(&mut self, target: u32, delta: kozan_primitives::geometry::Offset) -> bool {
208 let scrolled = ScrollController::new(&self.scroll_tree, &mut self.scroll_offsets)
209 .scroll(target, delta);
210 if scrolled.is_empty() {
211 return false;
212 }
213 self.dirty.invalidate_paint();
214 self.event_handler.invalidate_hit_cache();
215 self.event_handler.suppress_hover();
216
217 for node_id in scrolled.iter() {
218 let offset = self.scroll_offsets.offset(node_id);
219 if let Some(handle) = self.doc.handle_for_index(node_id) {
220 handle.dispatch_event(&ScrollEvent {
221 scroll_x: offset.dx,
222 scroll_y: offset.dy,
223 });
224 }
225 }
226 true
227 }
228
229 pub fn apply_compositor_scroll(&mut self, offsets: &ScrollOffsets) {
234 for (node_id, offset) in offsets.iter() {
235 let current = self.scroll_offsets.offset(node_id);
236 if current != *offset {
237 self.scroll_offsets.set_offset(node_id, *offset);
238 self.dirty.invalidate_paint();
239 self.event_handler.invalidate_hit_cache();
240 }
241 }
242 }
243
244 pub fn update_lifecycle(&mut self) {
252 if self.doc.needs_visual_update() || self.needs_full_layout {
253 self.dirty.invalidate_all();
254 }
255
256 if !self.dirty.needs_update() && self.last_fragment.is_some() {
257 return;
258 }
259
260 let t0 = std::time::Instant::now();
261 let mut style_ms = 0.0;
262 let mut layout_ms = 0.0;
263
264 if self.dirty.needs_style() {
265 self.lifecycle = LifecycleState::InStyleRecalc;
266 let t = std::time::Instant::now();
267 self.doc.recalc_styles();
268 style_ms = t.elapsed().as_secs_f64() * 1000.0;
269 self.lifecycle = LifecycleState::StyleClean;
270 self.dirty.clear_style();
271 }
272
273 if self.dirty.needs_layout() || self.last_fragment.is_none() {
274 self.lifecycle = LifecycleState::InLayout;
275 let t = std::time::Instant::now();
276 self.layout_pass();
277 layout_ms = t.elapsed().as_secs_f64() * 1000.0;
278 self.lifecycle = LifecycleState::LayoutClean;
279 self.dirty.clear_layout();
280 }
281
282 if self.dirty.needs_paint() {
283 self.lifecycle = LifecycleState::InPaint;
284 let t = std::time::Instant::now();
285 self.paint_pass();
286 let paint_ms = t.elapsed().as_secs_f64() * 1000.0;
287 self.lifecycle = LifecycleState::PaintClean;
288 self.dirty.clear_paint();
289
290 self.last_timing = kozan_primitives::timing::FrameTiming {
291 style_ms,
292 layout_ms,
293 paint_ms,
294 total_ms: t0.elapsed().as_secs_f64() * 1000.0,
295 };
296 }
297 }
298
299 fn layout_pass(&mut self) {
304 if self.viewport.width() == 0 || self.viewport.height() == 0 {
305 return;
306 }
307
308 let vw = self.viewport.logical_width() as f32;
309 let vh = self.viewport.logical_height() as f32;
310
311 let layout_dirty = self.doc.take_layout_dirty() || self.needs_full_layout;
312 self.needs_full_layout = false;
313
314 let ctx = LayoutContext {
315 text_measurer: &self.font_system,
316 };
317 let root = self.doc.root_index();
318 let result = self
319 .doc
320 .resolve_layout_dirty(root, Some(vw), Some(vh), &ctx, layout_dirty);
321 self.last_fragment = Some(result.fragment);
322
323 if let Some(frag) = &self.last_fragment {
325 self.scroll_tree.sync(frag);
326 }
327 }
328
329 fn paint_pass(&mut self) {
333 let Some(fragment) = &self.last_fragment else {
334 return;
335 };
336
337 if let Some(painted) = &self.painted_fragment {
340 if Arc::ptr_eq(painted, fragment) && !self.dirty.needs_paint() {
341 return;
342 }
343 }
344
345 let viewport_size = kozan_primitives::geometry::Size::new(
346 self.viewport.logical_width() as f32,
347 self.viewport.logical_height() as f32,
348 );
349
350 let display_list = Painter::new(&self.scroll_offsets).paint(fragment, viewport_size);
351 self.last_display_list = Some(Arc::new(display_list));
352 self.painted_fragment = Some(Arc::clone(fragment));
353
354 self.last_layer_tree = Some(LayerTreeBuilder::new(&self.scroll_offsets).build(fragment));
356 }
357
358 #[inline]
360 pub fn last_display_list(&self) -> Option<Arc<DisplayList>> {
361 self.last_display_list.as_ref().map(Arc::clone)
362 }
363
364 pub fn take_layer_tree(&mut self) -> Option<LayerTree> {
366 self.last_layer_tree.take()
367 }
368
369 pub fn scroll_state_snapshot(&self) -> (ScrollTree, ScrollOffsets) {
372 (self.scroll_tree.clone(), self.scroll_offsets.clone())
373 }
374
375 pub fn resize(&mut self, width: u32, height: u32) {
377 self.viewport.resize(width, height);
378 let lw = self.viewport.logical_width() as f32;
379 let lh = self.viewport.logical_height() as f32;
380 self.doc.set_viewport(lw, lh);
381 self.dirty.invalidate_layout();
386 }
387
388 pub fn set_scale_factor(&mut self, factor: f64) {
389 self.viewport.set_scale_factor(factor);
390 self.dirty.invalidate_all();
391 self.needs_full_layout = true;
392 }
393
394 pub fn mark_needs_update(&mut self) {
400 self.dirty.invalidate_all();
401 }
402
403 #[inline]
404 pub fn last_timing(&self) -> kozan_primitives::timing::FrameTiming {
405 self.last_timing
406 }
407
408 pub fn set_focus(&mut self, _focused: bool) {
409 }
411}
412
413impl Default for FrameWidget {
414 fn default() -> Self {
415 Self::new()
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::input::{InputEvent, Modifiers, mouse::MouseMoveEvent as RawMouseMoveEvent};
423 use std::time::Instant;
424
425 #[test]
426 fn construction() {
427 let widget = FrameWidget::new();
428 assert_eq!(widget.viewport().width(), 0);
429 assert_eq!(widget.viewport().height(), 0);
430 assert_eq!(widget.viewport().scale_factor(), 1.0);
431 assert_eq!(widget.lifecycle(), LifecycleState::PaintClean);
432 }
433
434 #[test]
435 fn resize_updates_viewport() {
436 let mut widget = FrameWidget::new();
437 widget.resize(1920, 1080);
438 assert_eq!(widget.viewport().width(), 1920);
439 assert_eq!(widget.viewport().height(), 1080);
440 }
441
442 #[test]
443 fn resize_invalidates_dirty() {
444 let mut widget = FrameWidget::new();
445 widget.resize(800, 600);
446 assert!(widget.dirty.needs_update());
447 }
448
449 #[test]
450 fn scale_factor() {
451 let mut widget = FrameWidget::new();
452 widget.set_scale_factor(2.0);
453 assert_eq!(widget.viewport().scale_factor(), 2.0);
454 }
455
456 #[test]
457 fn handle_input_without_fragment() {
458 let mut widget = FrameWidget::new();
459 let changed = widget.handle_input(InputEvent::MouseMove(RawMouseMoveEvent {
460 x: 100.0,
461 y: 200.0,
462 modifiers: Modifiers::EMPTY,
463 timestamp: Instant::now(),
464 }));
465 assert!(!changed);
466 }
467
468 #[test]
469 fn update_lifecycle_runs_all_phases() {
470 let mut widget = FrameWidget::new();
471 widget.resize(800, 600);
472 widget.update_lifecycle();
473
474 assert_eq!(widget.lifecycle(), LifecycleState::PaintClean);
475 assert!(
476 widget.last_fragment().is_some(),
477 "layout should produce a fragment after lifecycle"
478 );
479 }
480
481 #[test]
482 fn update_lifecycle_skips_when_clean() {
483 let mut widget = FrameWidget::new();
484 widget.resize(800, 600);
485
486 widget.update_lifecycle();
487 let frag1 = widget.last_fragment().cloned();
488
489 widget.update_lifecycle();
490 let frag2 = widget.last_fragment().cloned();
491
492 assert!(
493 Arc::ptr_eq(
494 frag1.as_ref().expect("frag1"),
495 frag2.as_ref().expect("frag2")
496 ),
497 "clean lifecycle should not re-layout"
498 );
499 }
500
501 #[test]
502 fn resize_triggers_relayout() {
503 let mut widget = FrameWidget::new();
504 widget.resize(800, 600);
505 widget.update_lifecycle();
506 let frag1 = widget.last_fragment().cloned();
507
508 widget.resize(1024, 768);
509 widget.update_lifecycle();
510 let frag2 = widget.last_fragment().cloned();
511
512 assert!(
513 !Arc::ptr_eq(
514 frag1.as_ref().expect("frag1"),
515 frag2.as_ref().expect("frag2")
516 ),
517 "resize should trigger relayout"
518 );
519 }
520
521 #[test]
522 fn document_access() {
523 let widget = FrameWidget::new();
524 let _doc = widget.document();
525 }
526
527 #[test]
528 fn no_layout_without_resize() {
529 let mut widget = FrameWidget::new();
530 widget.update_lifecycle();
531 assert!(
532 widget.last_fragment().is_none(),
533 "should not layout with zero viewport"
534 );
535 }
536}