1use std::any::Any;
8use std::borrow::Cow;
9use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11use std::num::NonZeroU64;
12use std::sync::Mutex;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::Duration;
15
16pub use frust_scene::{CornerRadii, DashPattern};
18use frust_scene::{GlyphRun, SceneBuilder, ShaderProgram};
19use kurbo::{Affine, BezPath, Point, Rect, Size};
20use peniko::{Brush, Color};
21
22use crate::anim::FrameTime;
23use crate::event::{EventCtx, EventResult, ImeState, InputEvent};
24use crate::insets::WindowInsets;
25use crate::layout::BoxConstraints;
26use crate::semantics::SemanticsCtx;
27use crate::shortcuts::ShortcutId;
28use crate::sliver::{SliverConstraints, SliverGeometry, SliverWidget};
29
30pub trait PaintScene {
35 fn fill_rect(&mut self, origin: Point, size: Size, color: Color);
37
38 fn fill_rounded_rect(&mut self, _origin: Point, _size: Size, _radius: f64, _color: Color) {}
40
41 fn fill_rounded_rect_radii(
43 &mut self,
44 origin: Point,
45 size: Size,
46 radii: CornerRadii,
47 color: Color,
48 ) {
49 self.fill_rounded_rect(origin, size, radii.largest(), color);
50 }
51
52 fn stroke_line(&mut self, _p0: Point, _p1: Point, _width: f64, _color: Color) {}
54
55 fn push_clip(&mut self, _origin: Point, _size: Size) {}
57
58 fn push_clip_rounded(&mut self, _origin: Point, _size: Size, _radius: f64) {}
60
61 fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
64 self.push_clip_rounded(origin, size, radii.largest());
65 }
66
67 fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
70 let bounds = kurbo::Shape::bounding_box(path);
71 self.push_clip(origin + bounds.origin().to_vec2(), bounds.size());
72 }
73
74 fn pop_clip(&mut self) {}
76
77 fn draw_text(&mut self, origin: Point, text: &str);
79
80 fn draw_glyph_run(&mut self, _run: GlyphRun) {}
82
83 fn draw_image(&mut self, _data: &peniko::ImageData, _dest: Rect) {}
85
86 fn draw_shader(&mut self, _program: &ShaderProgram, _dest: Rect, _time: f32) {}
91
92 fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
95
96 fn draw_shadow(
98 &mut self,
99 _origin: Point,
100 _size: Size,
101 _radius: f64,
102 _std_dev: f64,
103 _color: Color,
104 ) {
105 }
106
107 fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
110 let color = match brush {
111 Brush::Solid(color) => *color,
112 _ => Color::TRANSPARENT,
113 };
114 self.fill_rect(origin, size, color);
115 }
116
117 fn fill_rounded_rect_brush(
119 &mut self,
120 _origin: Point,
121 _size: Size,
122 _radius: f64,
123 _brush: &Brush,
124 ) {
125 }
126
127 fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {}
129
130 fn pop_layer(&mut self) {}
132
133 fn clear_rect(&mut self, _origin: Point, _size: Size) {}
136
137 fn fill_path(&mut self, _origin: Point, _path: &BezPath, _brush: &Brush) {}
139
140 fn stroke_path(&mut self, _origin: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
142
143 fn stroke_path_dashed(
145 &mut self,
146 origin: Point,
147 path: &BezPath,
148 width: f64,
149 _dash: DashPattern,
150 brush: &Brush,
151 ) {
152 self.stroke_path(origin, path, width, brush);
153 }
154
155 fn push_transform(&mut self, _transform: Affine) {}
158
159 fn pop_transform(&mut self) {}
161
162 fn push_retained(&mut self, _key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
165 let center = Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
166 self.push_transform(Affine::scale_about(scale, center));
167 self.push_layer(origin, size, alpha);
168 }
169
170 fn pop_retained(&mut self) {
172 self.pop_layer();
173 self.pop_transform();
174 }
175
176 fn draw_retained(&mut self, _key: u64, _origin: Point, _size: Size, _alpha: f32, _scale: f64) {}
178
179 fn current_transform(&self) -> Option<Affine> {
182 None
183 }
184
185 fn backdrop_blur(&mut self, _origin: Point, _size: Size, _radius: f64) {}
188
189 fn command_index(&self) -> usize {
192 0
193 }
194}
195
196pub struct DiscardScene;
199
200impl PaintScene for DiscardScene {
201 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
202
203 fn draw_text(&mut self, _origin: Point, _text: &str) {}
204}
205
206impl PaintScene for SceneBuilder<'_> {
208 fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
209 SceneBuilder::fill_rect(self, rect_at(origin, size), Brush::Solid(color));
210 }
211
212 fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, color: Color) {
213 SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, Brush::Solid(color));
214 }
215
216 fn fill_rounded_rect_radii(
217 &mut self,
218 origin: Point,
219 size: Size,
220 radii: CornerRadii,
221 color: Color,
222 ) {
223 SceneBuilder::fill_rounded_rect_radii(
224 self,
225 rect_at(origin, size),
226 radii,
227 Brush::Solid(color),
228 );
229 }
230
231 fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, color: Color) {
232 SceneBuilder::stroke_line(self, p0, p1, width, Brush::Solid(color));
233 }
234
235 fn push_clip(&mut self, origin: Point, size: Size) {
236 SceneBuilder::push_clip(self, rect_at(origin, size));
237 }
238
239 fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
240 SceneBuilder::push_clip_rounded(self, rect_at(origin, size), radius);
241 }
242
243 fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
244 SceneBuilder::push_clip_rounded_radii(self, rect_at(origin, size), radii);
245 }
246
247 fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
248 SceneBuilder::push_clip_path(self, path_at(origin, path));
249 }
250
251 fn pop_clip(&mut self) {
252 SceneBuilder::pop_clip(self);
253 }
254
255 fn draw_text(&mut self, _origin: Point, _text: &str) {
256 }
258
259 fn draw_glyph_run(&mut self, run: GlyphRun) {
260 SceneBuilder::draw_glyph_run(self, run);
261 }
262
263 fn draw_image(&mut self, data: &peniko::ImageData, dest: Rect) {
264 SceneBuilder::draw_image(self, data, dest);
265 }
266
267 fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
268 SceneBuilder::draw_shader(self, program, dest, time);
269 }
270
271 fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
272 SceneBuilder::scene_texture(self, id, dest);
273 }
274
275 fn draw_shadow(&mut self, origin: Point, size: Size, radius: f64, std_dev: f64, color: Color) {
276 SceneBuilder::draw_blurred_rounded_rect(
277 self,
278 rect_at(origin, size),
279 radius,
280 std_dev,
281 color,
282 );
283 }
284
285 fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
286 SceneBuilder::fill_rect(self, rect_at(origin, size), brush.clone());
287 }
288
289 fn fill_rounded_rect_brush(&mut self, origin: Point, size: Size, radius: f64, brush: &Brush) {
290 SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, brush.clone());
291 }
292
293 fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
294 SceneBuilder::push_layer(self, rect_at(origin, size), alpha);
295 }
296
297 fn pop_layer(&mut self) {
298 SceneBuilder::pop_layer(self);
299 }
300
301 fn clear_rect(&mut self, origin: Point, size: Size) {
302 SceneBuilder::clear_rect(self, rect_at(origin, size));
303 }
304
305 fn fill_path(&mut self, origin: Point, path: &BezPath, brush: &Brush) {
306 SceneBuilder::fill_path(self, path_at(origin, path), brush.clone());
307 }
308
309 fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, brush: &Brush) {
310 SceneBuilder::stroke_path(self, path_at(origin, path), width, brush.clone());
311 }
312
313 fn stroke_path_dashed(
314 &mut self,
315 origin: Point,
316 path: &BezPath,
317 width: f64,
318 dash: DashPattern,
319 brush: &Brush,
320 ) {
321 SceneBuilder::stroke_path_dashed(self, path_at(origin, path), width, dash, brush.clone());
322 }
323
324 fn push_transform(&mut self, transform: Affine) {
325 SceneBuilder::push_transform(self, transform);
326 }
327
328 fn pop_transform(&mut self) {
329 SceneBuilder::pop_transform(self);
330 }
331
332 fn push_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
333 SceneBuilder::push_retained(self, key, rect_at(origin, size), alpha, scale);
334 }
335
336 fn pop_retained(&mut self) {
337 SceneBuilder::pop_retained(self);
338 }
339
340 fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
341 SceneBuilder::draw_retained(self, key, rect_at(origin, size), alpha, scale);
342 }
343
344 fn current_transform(&self) -> Option<Affine> {
345 Some(SceneBuilder::current_transform(self))
346 }
347
348 fn backdrop_blur(&mut self, origin: Point, size: Size, radius: f64) {
349 SceneBuilder::backdrop_blur(self, rect_at(origin, size), radius);
350 }
351
352 fn command_index(&self) -> usize {
353 SceneBuilder::command_count(self)
354 }
355}
356
357fn rect_at(origin: Point, size: Size) -> Rect {
359 Rect::new(
360 origin.x,
361 origin.y,
362 origin.x + size.width,
363 origin.y + size.height,
364 )
365}
366
367fn path_at(origin: Point, path: &BezPath) -> BezPath {
369 Affine::translate((origin.x, origin.y)) * path.clone()
370}
371
372pub struct LayoutCtx<'a> {
376 text_ctx: Option<&'a mut dyn Any>,
377 theme: Option<&'a dyn Any>,
379 window_insets: WindowInsets,
381 window_size: Size,
383 epoch: u64,
385 force: bool,
387}
388
389impl<'a> LayoutCtx<'a> {
390 pub fn new() -> LayoutCtx<'static> {
392 LayoutCtx {
393 text_ctx: None,
394 theme: None,
395 window_insets: WindowInsets::default(),
396 window_size: Size::ZERO,
397 epoch: 0,
398 force: true,
399 }
400 }
401
402 pub fn with_text_context(text_ctx: &'a mut dyn Any) -> Self {
404 LayoutCtx {
405 text_ctx: Some(text_ctx),
406 theme: None,
407 window_insets: WindowInsets::default(),
408 window_size: Size::ZERO,
409 epoch: 0,
410 force: true,
411 }
412 }
413
414 pub fn with_resources(text_ctx: Option<&'a mut dyn Any>, theme: Option<&'a dyn Any>) -> Self {
416 LayoutCtx {
417 text_ctx,
418 theme,
419 window_insets: WindowInsets::default(),
420 window_size: Size::ZERO,
421 epoch: 0,
422 force: true,
423 }
424 }
425
426 pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
428 self.theme = Some(theme);
429 self
430 }
431
432 pub fn text_context<T: Any>(&mut self) -> &mut T {
435 self.text_ctx
436 .as_deref_mut()
437 .expect("no text context threaded into this layout pass")
438 .downcast_mut::<T>()
439 .expect("threaded layout resource is not the expected text-context type")
440 }
441
442 pub fn theme_as<T: Any>(&self) -> Option<&T> {
444 self.theme?.downcast_ref::<T>()
445 }
446
447 pub fn window_insets(&self) -> WindowInsets {
449 self.window_insets
450 }
451
452 pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
454 self.window_insets = insets;
455 }
456
457 pub fn with_window_insets<R>(
460 &mut self,
461 insets: WindowInsets,
462 f: impl FnOnce(&mut Self) -> R,
463 ) -> R {
464 let saved = std::mem::replace(&mut self.window_insets, insets);
465 let result = f(self);
466 self.window_insets = saved;
467 result
468 }
469
470 pub fn window_size(&self) -> Size {
473 self.window_size
474 }
475
476 pub(crate) fn set_window_size(&mut self, size: Size) {
478 self.window_size = size;
479 }
480
481 pub fn layout_epoch(&self) -> u64 {
483 self.epoch
484 }
485
486 pub(crate) fn set_layout_epoch(&mut self, epoch: u64) {
488 self.epoch = epoch;
489 }
490
491 pub fn is_forced(&self) -> bool {
493 self.force
494 }
495
496 pub(crate) fn set_layout_force(&mut self, force: bool) {
498 self.force = force;
499 }
500}
501
502impl Default for LayoutCtx<'static> {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511pub enum TickClass {
512 CosmeticLoop,
514 Transition,
516}
517
518pub struct PaintCtx<'a> {
521 origin: Point,
522 size: Size,
523 needs_frame: bool,
524 needs_layout: bool,
526 layout_path_dirty: bool,
529 frame_unpaced: bool,
531 paced_interval: Option<Duration>,
533 ime_state: Option<ImeState>,
534 has_focus: bool,
536 focus_ring: bool,
538 hovered: bool,
540 hover_epoch: u64,
541 focus_epoch: u64,
543 frame_time: FrameTime,
544 theme: Option<&'a dyn Any>,
545 window_insets: WindowInsets,
546 presented_frames: Option<u64>,
548 hero: Option<&'a RefCell<HeroFrames>>,
550 visible_rect: Option<Rect>,
552 platform_views: Vec<PlatformViewFrame>,
554 input_shields: Vec<Rect>,
556 translucent: bool,
557 paint_epoch: u64,
560 retained_layers: bool,
562 retained_ledger: Option<&'a crate::retained::RetainedLedger>,
564 paint_serial: u64,
566 paint_publications: u32,
568 shortcut_hints: Option<std::rc::Rc<[crate::shortcuts::ShortcutHint]>>,
570}
571
572impl<'a> PaintCtx<'a> {
573 pub fn new(origin: Point, size: Size) -> Self {
575 Self {
576 origin,
577 size,
578 needs_frame: false,
579 needs_layout: false,
580 layout_path_dirty: false,
581 frame_unpaced: false,
582 paced_interval: None,
583 ime_state: None,
584 has_focus: false,
585 focus_ring: false,
586 hovered: false,
587 hover_epoch: 0,
588 focus_epoch: 0,
589 frame_time: FrameTime::ZERO,
590 theme: None,
591 window_insets: WindowInsets::default(),
592 presented_frames: None,
593 hero: None,
594 visible_rect: None,
595 platform_views: Vec::new(),
596 input_shields: Vec::new(),
597 translucent: false,
598 paint_epoch: 0,
599 retained_layers: true,
600 retained_ledger: None,
601 paint_serial: 0,
602 paint_publications: 0,
603 shortcut_hints: None,
604 }
605 }
606
607 pub(crate) fn with_shortcut_hints(
609 mut self,
610 hints: std::rc::Rc<[crate::shortcuts::ShortcutHint]>,
611 ) -> Self {
612 self.shortcut_hints = Some(hints);
613 self
614 }
615
616 pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
618 self.theme = Some(theme);
619 self
620 }
621
622 pub fn with_translucent(mut self, translucent: bool) -> Self {
624 self.translucent = translucent;
625 self
626 }
627
628 pub fn with_retained_ledger(mut self, ledger: &'a crate::retained::RetainedLedger) -> Self {
630 self.retained_ledger = Some(ledger);
631 self
632 }
633
634 #[cfg(any(test, feature = "test-support"))]
637 pub fn for_test(origin: Point, size: Size, frame_time: FrameTime) -> Self {
638 let mut ctx = Self::new(origin, size);
639 ctx.frame_time = frame_time;
640 ctx
641 }
642
643 pub fn theme_as<T: Any>(&self) -> Option<&T> {
645 self.theme?.downcast_ref::<T>()
646 }
647
648 pub(crate) fn set_theme(&mut self, theme: Option<&'a dyn Any>) {
650 self.theme = theme;
651 }
652
653 pub(crate) fn theme_ref(&self) -> Option<&'a dyn Any> {
655 self.theme
656 }
657
658 pub fn window_insets(&self) -> WindowInsets {
660 self.window_insets
661 }
662
663 pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
665 self.window_insets = insets;
666 }
667
668 pub(crate) fn window_insets_ref(&self) -> WindowInsets {
670 self.window_insets
671 }
672
673 pub fn with_window_insets<R>(
676 &mut self,
677 insets: WindowInsets,
678 f: impl FnOnce(&mut Self) -> R,
679 ) -> R {
680 let saved = std::mem::replace(&mut self.window_insets, insets);
681 let result = f(self);
682 self.window_insets = saved;
683 result
684 }
685
686 pub fn presented_frames(&self) -> Option<u64> {
689 self.presented_frames
690 }
691
692 pub(crate) fn set_presented_frames(&mut self, presented: Option<u64>) {
694 self.presented_frames = presented;
695 }
696
697 pub fn origin(&self) -> Point {
700 self.origin
701 }
702
703 pub fn size(&self) -> Size {
705 self.size
706 }
707
708 pub fn has_focus(&self) -> bool {
711 self.has_focus
712 }
713
714 pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
716 self.has_focus = has_focus;
717 }
718
719 pub fn shortcut_hints(&self) -> std::rc::Rc<[crate::shortcuts::ShortcutHint]> {
722 self.shortcut_hints
723 .clone()
724 .unwrap_or_else(|| std::rc::Rc::from(Vec::new()))
725 }
726
727 pub fn is_focused_for_ring(&self) -> bool {
730 self.has_focus && self.focus_ring
731 }
732
733 pub(crate) fn set_focus_ring(&mut self, visible: bool) {
734 self.focus_ring = visible;
735 }
736
737 pub fn is_hovered(&self) -> bool {
741 self.hovered
742 }
743
744 pub(crate) fn set_hovered(&mut self, hovered: bool) {
746 self.hovered = hovered;
747 }
748
749 pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
751 self.hover_epoch = epoch;
752 }
753
754 pub(crate) fn hover_epoch(&self) -> u64 {
756 self.hover_epoch
757 }
758
759 pub(crate) fn set_focus_epoch(&mut self, epoch: u64) {
761 self.focus_epoch = epoch;
762 }
763
764 pub(crate) fn focus_epoch(&self) -> u64 {
766 self.focus_epoch
767 }
768
769 pub fn frame_time(&self) -> FrameTime {
772 self.frame_time
773 }
774
775 pub(crate) fn set_frame_time(&mut self, frame_time: FrameTime) {
777 self.frame_time = frame_time;
778 }
779
780 pub fn request_frame(&mut self) {
783 self.request_frame_class(TickClass::Transition);
784 }
785
786 pub fn request_frame_paced(&mut self) {
789 self.request_frame_paced_at(Duration::ZERO);
790 }
791
792 pub fn request_frame_paced_at(&mut self, interval: Duration) {
796 self.needs_frame = true;
797 self.merge_paced_interval(interval.min(Self::MAX_PACED_INTERVAL));
798 }
799
800 pub const MAX_PACED_INTERVAL: Duration = Duration::from_secs(10);
803
804 pub fn request_frame_class(&mut self, class: TickClass) {
806 match class {
807 TickClass::Transition => {
808 self.needs_frame = true;
809 self.frame_unpaced = true;
810 }
811 TickClass::CosmeticLoop => self.request_frame_paced(),
812 }
813 }
814
815 fn merge_paced_interval(&mut self, interval: Duration) {
817 self.paced_interval = Some(match self.paced_interval {
818 Some(current) => current.min(interval),
819 None => interval,
820 });
821 }
822
823 fn absorb_paced_interval(&mut self, interval: Option<Duration>) {
826 if let Some(interval) = interval {
827 self.merge_paced_interval(interval);
828 }
829 }
830
831 pub fn needs_frame(&self) -> bool {
833 self.needs_frame
834 }
835
836 pub fn frame_class(&self) -> Option<TickClass> {
838 if !self.needs_frame {
839 None
840 } else if self.frame_unpaced {
841 Some(TickClass::Transition)
842 } else {
843 Some(TickClass::CosmeticLoop)
844 }
845 }
846
847 pub fn needs_frame_paced_only(&self) -> bool {
849 self.needs_frame && !self.frame_unpaced
850 }
851
852 pub fn paced_interval(&self) -> Option<Duration> {
854 self.paced_interval
855 }
856
857 pub fn request_layout(&mut self) {
860 self.needs_layout = true;
861 self.request_frame_class(TickClass::Transition);
862 }
863
864 pub fn needs_layout(&self) -> bool {
866 self.needs_layout
867 }
868
869 pub(crate) fn is_layout_path_dirty(&self) -> bool {
871 self.layout_path_dirty
872 }
873
874 pub fn publish_ime_state(&mut self, state: ImeState) {
877 self.note_publication();
878 self.ime_state = Some(state);
879 }
880
881 pub fn take_ime_state(&mut self) -> Option<ImeState> {
883 self.ime_state.take()
884 }
885
886 pub fn publish_platform_view(&mut self, frame: PlatformViewFrame) {
888 self.note_publication();
889 self.platform_views.push(frame);
890 }
891
892 pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame> {
894 std::mem::take(&mut self.platform_views)
895 }
896
897 pub fn report_input_shield(&mut self, rect: Rect) {
900 self.note_publication();
901 self.input_shields.push(rect);
902 }
903
904 pub fn take_input_shields(&mut self) -> Vec<Rect> {
906 std::mem::take(&mut self.input_shields)
907 }
908
909 pub fn register_overlay(&mut self, entry: crate::overlay::OverlayEntry) {
913 self.note_publication();
914 crate::overlay::register(entry);
915 }
916
917 pub fn register_shortcut(&mut self, decl: crate::shortcuts::ShortcutDecl) {
921 self.note_publication();
922 crate::shortcuts::register(decl);
923 }
924
925 pub fn publish_selection_toolbar(
929 &mut self,
930 request: crate::selection_toolbar::SelectionToolbarRequest,
931 ) {
932 self.note_publication();
933 crate::selection_toolbar::publish(request);
934 }
935
936 pub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective {
939 self.note_publication();
940 match self.hero {
941 Some(cell) => {
942 let mut frames = cell.borrow_mut();
943 let local = Rect::from_origin_size(
944 bounds.origin() - frames.reference.to_vec2(),
945 bounds.size(),
946 );
947 frames.captured.insert(tag.to_string(), local);
948 frames
949 .directives
950 .get(tag)
951 .copied()
952 .unwrap_or(HeroDirective::Normal)
953 }
954 None => HeroDirective::Normal,
955 }
956 }
957
958 pub fn hero_active(&self) -> bool {
960 self.hero.is_some()
961 }
962
963 pub fn with_hero_registry(
966 &mut self,
967 registry: &RefCell<HeroFrames>,
968 f: impl FnOnce(&mut PaintCtx),
969 ) {
970 let mut child = PaintCtx {
971 origin: self.origin,
972 size: self.size,
973 needs_frame: false,
974 needs_layout: false,
975 layout_path_dirty: false,
976 frame_unpaced: false,
977 paced_interval: None,
978 ime_state: None,
979 has_focus: self.has_focus,
980 focus_ring: self.focus_ring,
981 hovered: self.hovered,
982 hover_epoch: self.hover_epoch,
983 focus_epoch: self.focus_epoch,
984 frame_time: self.frame_time,
985 theme: self.theme,
986 window_insets: self.window_insets,
987 presented_frames: self.presented_frames,
988 hero: Some(registry),
989 visible_rect: self.visible_rect,
990 platform_views: Vec::new(),
991 input_shields: Vec::new(),
992 translucent: self.translucent,
993 paint_epoch: self.paint_epoch,
994 retained_layers: self.retained_layers,
995 retained_ledger: self.retained_ledger,
996 paint_serial: self.paint_serial,
997 paint_publications: 0,
998 shortcut_hints: self.shortcut_hints.clone(),
999 };
1000 f(&mut child);
1001 self.paint_publications = self
1002 .paint_publications
1003 .saturating_add(child.paint_publications);
1004 if child.needs_frame {
1005 self.needs_frame = true;
1006 }
1007 if child.needs_layout {
1008 self.needs_layout = true;
1009 }
1010 if child.layout_path_dirty {
1011 self.layout_path_dirty = true;
1012 }
1013 if child.frame_unpaced {
1015 self.frame_unpaced = true;
1016 }
1017 self.absorb_paced_interval(child.paced_interval);
1018 if let Some(ime) = child.ime_state.take() {
1019 self.ime_state = Some(ime);
1020 }
1021 self.platform_views.extend(child.platform_views);
1022 self.input_shields.extend(child.input_shields);
1023 }
1024
1025 pub(crate) fn set_hero(&mut self, hero: Option<&'a RefCell<HeroFrames>>) {
1027 self.hero = hero;
1028 }
1029
1030 pub(crate) fn hero_ref(&self) -> Option<&'a RefCell<HeroFrames>> {
1032 self.hero
1033 }
1034
1035 pub fn visible_rect(&self) -> Option<Rect> {
1037 self.visible_rect
1038 }
1039
1040 pub fn constrain_visible_rect(&mut self, rect: Rect) {
1042 self.visible_rect = Some(match self.visible_rect {
1043 Some(existing) => existing.intersect(rect),
1044 None => rect,
1045 });
1046 }
1047
1048 pub(crate) fn set_visible_rect(&mut self, rect: Option<Rect>) {
1050 self.visible_rect = rect;
1051 }
1052
1053 pub(crate) fn visible_rect_ref(&self) -> Option<Rect> {
1055 self.visible_rect
1056 }
1057
1058 pub fn is_translucent(&self) -> bool {
1061 self.translucent
1062 }
1063
1064 pub(crate) fn set_translucent(&mut self, translucent: bool) {
1066 self.translucent = translucent;
1067 }
1068
1069 pub(crate) fn set_paint_state(
1071 &mut self,
1072 paint_epoch: u64,
1073 retained_layers: bool,
1074 retained_ledger: Option<&'a crate::retained::RetainedLedger>,
1075 paint_serial: u64,
1076 ) {
1077 self.paint_epoch = paint_epoch;
1078 self.retained_layers = retained_layers;
1079 self.retained_ledger = retained_ledger;
1080 self.paint_serial = paint_serial;
1081 }
1082
1083 pub fn paint_serial(&self) -> u64 {
1086 self.paint_serial
1087 }
1088
1089 pub fn retained_available(
1092 &self,
1093 key: crate::retained::RetainedKey,
1094 rect_px: kurbo::Size,
1095 scale: f32,
1096 recorded_serial: u64,
1097 ) -> bool {
1098 let extent = (
1099 rect_px.width.round().max(0.0) as u32,
1100 rect_px.height.round().max(0.0) as u32,
1101 );
1102 self.retained_layers
1103 && self
1104 .retained_ledger
1105 .is_some_and(|l| l.covers(recorded_serial) && l.is_resident(key, extent, scale))
1106 }
1107
1108 pub fn subtree_clean(&self, pod: &ChildPod) -> bool {
1111 self.retained_layers && !pod.paint_dirty && pod.painted_epoch == self.paint_epoch
1112 }
1113
1114 pub fn retained_enabled(&self) -> bool {
1116 self.retained_layers && self.retained_ledger.is_some()
1117 }
1118
1119 pub fn paint_publications(&self) -> u32 {
1122 self.paint_publications
1123 }
1124
1125 fn note_publication(&mut self) {
1127 self.paint_publications = self.paint_publications.saturating_add(1);
1128 }
1129}
1130
1131#[derive(Debug, Default)]
1134pub struct HeroFrames {
1135 reference: Point,
1137 captured: HashMap<String, Rect>,
1139 directives: HashMap<String, HeroDirective>,
1141}
1142
1143impl HeroFrames {
1144 pub fn new(reference: Point, directives: HashMap<String, HeroDirective>) -> Self {
1146 Self {
1147 reference,
1148 captured: HashMap::new(),
1149 directives,
1150 }
1151 }
1152
1153 pub fn captured(&self) -> &HashMap<String, Rect> {
1155 &self.captured
1156 }
1157
1158 pub fn into_captured(self) -> HashMap<String, Rect> {
1160 self.captured
1161 }
1162}
1163
1164#[derive(Clone, Copy, Debug, PartialEq)]
1166pub enum HeroDirective {
1167 Normal,
1169 Suppress,
1171 Morph {
1173 dest: Rect,
1175 },
1176}
1177
1178static NEXT_SLOT_ID: AtomicU64 = AtomicU64::new(1);
1179
1180pub fn next_slot_id() -> u64 {
1183 NEXT_SLOT_ID.fetch_add(1, Ordering::Relaxed)
1184}
1185
1186const MAX_PENDING_RETIRED_SLOTS: usize = 256;
1189
1190static RETIRED_SLOTS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
1192
1193pub fn report_retired_slot(slot_id: u64) {
1196 let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
1197 if pending.len() >= MAX_PENDING_RETIRED_SLOTS {
1198 pending.remove(0);
1199 }
1200 pending.push(slot_id);
1201}
1202
1203pub fn take_retired_slots() -> Vec<u64> {
1205 let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
1206 std::mem::take(&mut *pending)
1207}
1208
1209#[derive(Clone, Debug, PartialEq)]
1213pub struct PlatformViewFrame {
1214 pub slot_id: u64,
1216 pub view_type: String,
1218 pub params_json: String,
1220 pub params_generation: u64,
1222 pub rect: Rect,
1224 pub clip: Option<Rect>,
1226 pub visible: bool,
1228 pub interactive: bool,
1231 pub shields: Vec<Rect>,
1234 pub command_index: usize,
1237}
1238
1239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1241pub struct PaintOutcome {
1242 pub needs_frame: bool,
1244 pub needs_layout: bool,
1246 pub needs_frame_paced_only: bool,
1248 pub paced_interval: Option<Duration>,
1251 pub paint_serial: u64,
1253}
1254
1255pub trait Widget: Any {
1258 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size;
1264
1265 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene);
1268
1269 fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
1272 EventResult::Ignored
1273 }
1274
1275 fn semantics(&self, _ctx: &mut SemanticsCtx) {}
1278
1279 fn type_name(&self) -> &'static str {
1282 core::any::type_name::<Self>()
1283 }
1284
1285 fn visit_children(&self, _visitor: &mut dyn FnMut(&ChildPod)) {}
1288
1289 fn is_focusable(&self) -> bool {
1291 false
1292 }
1293
1294 fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1297 self.visit_children(visitor);
1298 }
1299
1300 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
1303 None
1304 }
1305}
1306
1307impl dyn Widget {
1308 pub fn downcast_mut<W: Widget>(&mut self) -> Option<&mut W> {
1310 let any: &mut dyn Any = self;
1311 any.downcast_mut::<W>()
1312 }
1313}
1314
1315impl Widget for Box<dyn Widget> {
1318 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1319 (**self).layout(ctx, bc)
1320 }
1321
1322 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1323 (**self).paint(ctx, scene);
1324 }
1325
1326 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1327 (**self).event(ctx, event)
1328 }
1329
1330 fn semantics(&self, ctx: &mut SemanticsCtx) {
1331 (**self).semantics(ctx);
1332 }
1333
1334 fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1335 (**self).visit_children(visitor);
1336 }
1337
1338 fn is_focusable(&self) -> bool {
1339 (**self).is_focusable()
1340 }
1341
1342 fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1343 (**self).visit_focus_children(visitor);
1344 }
1345
1346 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
1348 (**self).as_sliver()
1349 }
1350
1351 fn type_name(&self) -> &'static str {
1353 (**self).type_name()
1354 }
1355}
1356
1357pub struct ChildPod {
1361 widget: Box<dyn Widget>,
1362 origin: Point,
1363 size: Size,
1364 transform: Option<Affine>,
1366 active: bool,
1368 contacts_captor: bool,
1370 contacts_path: bool,
1372 focused: bool,
1375 focus_epoch: u64,
1378 focus_root: u64,
1380 hover_epoch: u64,
1383 hover_root: u64,
1385 semantics_id: Cell<Option<NonZeroU64>>,
1387 debug_label: Option<Cow<'static, str>>,
1389 inspect_id: Cell<Option<NonZeroU64>>,
1391 layout_dirty: bool,
1393 paint_dirty: bool,
1395 painted_epoch: u64,
1396 last_constraints: Option<BoxConstraints>,
1398 last_epoch: u64,
1399 last_insets: WindowInsets,
1400 painted_animating: bool,
1402 last_sliver_constraints: Option<SliverConstraints>,
1404 sliver_geometry: Option<SliverGeometry>,
1406}
1407
1408thread_local! {
1409 static NEXT_INSPECT_ID: Cell<u64> = const { Cell::new(ChildPod::INSPECT_ID_BASE) };
1411
1412 static HOVER_CLAIMANT: Cell<Option<u64>> = const { Cell::new(None) };
1414
1415 static FOCUS_REQ_FROM_LIVE: Cell<Option<bool>> = const { Cell::new(None) };
1417
1418 static LIVE_FOCUS_SESSION: Cell<(u64, u64, u64)> = const { Cell::new((0, 0, 0)) };
1422
1423 static FOCUS_GRANT: Cell<Option<(ShortcutId, u64, bool)>> = const { Cell::new(None) };
1425}
1426
1427pub(crate) struct FocusGrant;
1432
1433impl FocusGrant {
1434 pub(crate) fn enter(carrier: ShortcutId, target: u64) -> Self {
1435 FOCUS_GRANT.with(|grant| grant.set(Some((carrier, target, false))));
1436 FocusGrant
1437 }
1438}
1439
1440impl Drop for FocusGrant {
1441 fn drop(&mut self) {
1442 FOCUS_GRANT.with(|grant| grant.set(None));
1443 }
1444}
1445
1446pub(crate) fn set_live_focus_session(root: u64, live: u64, claim: u64) {
1450 LIVE_FOCUS_SESSION.with(|slot| slot.set((root, live, claim)));
1451}
1452
1453fn focus_claim_stamp() -> (u64, u64) {
1455 let (root, _live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
1456 (root, claim)
1457}
1458
1459fn names_live_focus_session(root: u64, epoch: u64) -> bool {
1461 let (live_root, live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
1462 root == live_root && (epoch == live || epoch == claim)
1463}
1464
1465impl Drop for ChildPod {
1466 fn drop(&mut self) {
1469 if crate::event::live_hover_link_is(self.hover_root, self.hover_epoch) {
1470 crate::event::mark_hover_orphaned(self.hover_root);
1471 }
1472 if self.focused
1475 && self.focus_epoch != 0
1476 && names_live_focus_session(self.focus_root, self.focus_epoch)
1477 {
1478 crate::event::mark_focus_orphaned();
1479 }
1480 }
1481}
1482
1483pub(crate) fn clear_hover_claimant() {
1485 HOVER_CLAIMANT.with(|claimant| claimant.set(None));
1486}
1487
1488pub(crate) fn take_hover_claimant() -> Option<u64> {
1490 HOVER_CLAIMANT.with(Cell::take)
1491}
1492
1493pub(crate) fn clear_focus_req_from_live() {
1495 FOCUS_REQ_FROM_LIVE.with(|c| c.set(None));
1496}
1497
1498pub(crate) fn take_focus_req_from_live() -> Option<bool> {
1500 FOCUS_REQ_FROM_LIVE.with(Cell::take)
1501}
1502
1503impl ChildPod {
1504 pub const INSPECT_ID_BASE: u64 = 1 << 48;
1507
1508 pub fn new(widget: Box<dyn Widget>) -> Self {
1510 Self {
1511 widget,
1512 origin: Point::ZERO,
1513 size: Size::ZERO,
1514 transform: None,
1515 active: false,
1516 contacts_captor: false,
1517 contacts_path: false,
1518 focused: false,
1519 focus_epoch: 0,
1520 focus_root: 0,
1521 hover_epoch: 0,
1522 hover_root: 0,
1523 semantics_id: Cell::new(None),
1524 debug_label: None,
1525 inspect_id: Cell::new(None),
1526 layout_dirty: true,
1527 paint_dirty: true,
1528 painted_epoch: 0,
1529 last_constraints: None,
1530 last_epoch: 0,
1531 last_insets: WindowInsets::default(),
1532 painted_animating: false,
1533 last_sliver_constraints: None,
1534 sliver_geometry: None,
1535 }
1536 }
1537
1538 pub fn type_name(&self) -> &'static str {
1541 self.widget.type_name()
1542 }
1543
1544 pub fn debug_label(&self) -> Option<&str> {
1546 self.debug_label.as_deref()
1547 }
1548
1549 pub fn set_debug_label(&mut self, label: impl Into<Cow<'static, str>>) {
1551 self.debug_label = Some(label.into());
1552 }
1553
1554 pub fn clear_debug_label(&mut self) {
1556 self.debug_label = None;
1557 }
1558
1559 pub(crate) fn debug_label_cow(&self) -> Option<Cow<'static, str>> {
1561 self.debug_label.clone()
1562 }
1563
1564 pub fn inspect_id(&self) -> crate::view::WidgetId {
1566 let id = match self.inspect_id.get() {
1567 Some(id) => id,
1568 None => {
1569 let id = NEXT_INSPECT_ID.with(|next| {
1570 let id = next.get();
1571 next.set(id + 1);
1572 NonZeroU64::new(id).expect("the allocator starts at 2^48, never zero")
1573 });
1574 self.inspect_id.set(Some(id));
1575 id
1576 }
1577 };
1578 crate::view::WidgetId(id.get())
1579 }
1580
1581 pub fn widget(&self) -> &dyn Widget {
1583 &*self.widget
1584 }
1585
1586 pub fn widget_mut(&mut self) -> &mut dyn Widget {
1588 self.layout_dirty = true;
1589 self.paint_dirty = true;
1590 &mut *self.widget
1591 }
1592
1593 pub fn widget_mut_for_rebuild(&mut self) -> &mut dyn Widget {
1595 &mut *self.widget
1596 }
1597
1598 pub fn set_widget(&mut self, widget: Box<dyn Widget>) {
1600 self.widget = widget;
1601 self.layout_dirty = true;
1602 self.paint_dirty = true;
1603 }
1604
1605 pub fn is_layout_dirty(&self) -> bool {
1607 self.layout_dirty
1608 }
1609
1610 pub fn mark_layout_dirty(&mut self) {
1613 self.layout_dirty = true;
1614 self.paint_dirty = true;
1615 }
1616
1617 pub fn is_paint_dirty(&self) -> bool {
1619 self.paint_dirty
1620 }
1621
1622 pub fn mark_paint_dirty(&mut self) {
1624 self.paint_dirty = true;
1625 }
1626
1627 pub fn origin(&self) -> Point {
1629 self.origin
1630 }
1631
1632 pub fn size(&self) -> Size {
1634 self.size
1635 }
1636
1637 pub fn set_origin(&mut self, origin: Point) {
1639 self.origin = origin;
1640 }
1641
1642 pub fn transform(&self) -> Option<Affine> {
1644 self.transform
1645 }
1646
1647 pub fn set_transform(&mut self, transform: Option<Affine>) {
1653 self.transform = transform;
1654 }
1655
1656 fn local_to_container(&self, transform: Affine) -> Affine {
1658 Affine::translate(self.origin.to_vec2()) * transform
1659 }
1660
1661 pub fn is_active(&self) -> bool {
1663 self.active
1664 }
1665
1666 pub fn set_active(&mut self, active: bool) {
1669 if !active && crate::event::in_secondary_contact_pass() {
1670 return;
1671 }
1672 self.active = active;
1673 if !active {
1674 self.contacts_captor = false;
1675 self.contacts_path = false;
1676 }
1677 }
1678
1679 pub(crate) fn holds_contact_opt_in(&self) -> bool {
1682 self.active && self.contacts_path
1683 }
1684
1685 pub fn is_focused(&self) -> bool {
1688 self.focused
1689 }
1690
1691 pub fn set_focused(&mut self, focused: bool) {
1693 self.focused = focused;
1694 if focused {
1695 let (root, epoch) = focus_claim_stamp();
1696 self.focus_root = root;
1697 self.focus_epoch = epoch;
1698 }
1699 }
1700
1701 pub fn focus_epoch(&self) -> u64 {
1703 self.focus_epoch
1704 }
1705
1706 pub fn holds_live_focus(&self) -> bool {
1709 self.focused && names_live_focus_session(self.focus_root, self.focus_epoch)
1710 }
1711
1712 pub fn retire_stale_focus_link(&mut self) -> bool {
1715 if self.focused && !self.holds_live_focus() {
1716 self.focused = false;
1717 true
1718 } else {
1719 false
1720 }
1721 }
1722
1723 pub fn hover_epoch(&self) -> u64 {
1726 self.hover_epoch
1727 }
1728
1729 pub fn layout_child(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1736 if !ctx.force
1737 && !self.layout_dirty
1738 && self.last_constraints == Some(*bc)
1739 && self.last_epoch == ctx.epoch
1740 && self.last_insets == ctx.window_insets
1741 {
1742 return self.size;
1743 }
1744 let size = self.widget.layout(ctx, bc);
1745 self.size = size;
1746 self.last_constraints = Some(*bc);
1747 self.last_sliver_constraints = None;
1748 self.sliver_geometry = None;
1749 self.last_epoch = ctx.epoch;
1750 self.last_insets = ctx.window_insets;
1751 self.layout_dirty = false;
1752 self.paint_dirty = true;
1753 size
1754 }
1755
1756 pub fn layout_sliver_child(
1760 &mut self,
1761 ctx: &mut LayoutCtx,
1762 sc: &SliverConstraints,
1763 ) -> Option<SliverGeometry> {
1764 if !ctx.force
1765 && !self.layout_dirty
1766 && self.last_sliver_constraints == Some(*sc)
1767 && self.last_epoch == ctx.epoch
1768 && self.last_insets == ctx.window_insets
1769 && let Some(geometry) = self.sliver_geometry
1770 {
1771 return Some(geometry);
1772 }
1773 sc.debug_assert_valid();
1774 let geometry = self.widget.as_sliver()?.layout_sliver(ctx, sc);
1775 geometry.debug_assert_valid(sc);
1776 self.size = sc.axis.size(geometry.paint_extent, sc.cross_axis_extent);
1777 self.sliver_geometry = Some(geometry);
1778 self.last_sliver_constraints = Some(*sc);
1779 self.last_constraints = None;
1780 self.last_epoch = ctx.epoch;
1781 self.last_insets = ctx.window_insets;
1782 self.layout_dirty = geometry.scroll_offset_correction.is_some();
1783 self.paint_dirty = true;
1784 Some(geometry)
1785 }
1786
1787 pub fn sliver_geometry(&self) -> Option<SliverGeometry> {
1789 self.sliver_geometry
1790 }
1791
1792 pub fn is_sliver(&mut self) -> bool {
1794 self.widget.as_sliver().is_some()
1795 }
1796
1797 pub fn paint_child(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1800 let Some(transform) = self.transform else {
1801 let visible_rect = ctx.visible_rect_ref();
1802 self.paint_child_in(ctx, scene, visible_rect);
1803 return;
1804 };
1805 if crate::hit::checked_inverse(&transform).is_none() {
1807 return;
1808 }
1809 let abs = (ctx.origin() + self.origin.to_vec2()).to_vec2();
1812 let around = Affine::translate(abs) * transform * Affine::translate(-abs);
1813 let visible_rect = match (ctx.visible_rect_ref(), crate::hit::checked_inverse(&around)) {
1815 (Some(rect), Some(inverse)) => Some(inverse.transform_rect_bbox(rect)),
1816 _ => None,
1817 };
1818 scene.push_transform(around);
1819 self.paint_child_in(ctx, scene, visible_rect);
1820 scene.pop_transform();
1821 }
1822
1823 fn paint_child_in(
1825 &mut self,
1826 ctx: &mut PaintCtx,
1827 scene: &mut dyn PaintScene,
1828 visible_rect: Option<Rect>,
1829 ) {
1830 let child_origin = ctx.origin() + self.origin.to_vec2();
1831 let mut child_ctx = PaintCtx::new(child_origin, self.size);
1832 child_ctx.set_frame_time(ctx.frame_time());
1834 child_ctx.set_theme(ctx.theme_ref());
1835 child_ctx.set_window_insets(ctx.window_insets_ref());
1836 child_ctx.set_presented_frames(ctx.presented_frames());
1837 child_ctx.set_hero(ctx.hero_ref());
1838 child_ctx.set_visible_rect(visible_rect);
1839 child_ctx.set_translucent(ctx.is_translucent());
1840 child_ctx.shortcut_hints = ctx.shortcut_hints.clone();
1841 child_ctx.set_paint_state(
1842 ctx.paint_epoch,
1843 ctx.retained_layers,
1844 ctx.retained_ledger,
1845 ctx.paint_serial,
1846 );
1847 child_ctx.set_focus_epoch(ctx.focus_epoch());
1850 child_ctx.set_has_focus(
1851 self.focused && self.focus_epoch == ctx.focus_epoch() && ctx.has_focus(),
1852 );
1853 child_ctx.set_focus_ring(ctx.focus_ring);
1854 child_ctx.set_hover_epoch(ctx.hover_epoch());
1856 child_ctx.set_hovered(self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered());
1857 self.widget.paint(&mut child_ctx, scene);
1858 ctx.paint_publications = ctx
1859 .paint_publications
1860 .saturating_add(child_ctx.paint_publications);
1861 match child_ctx.frame_class() {
1862 Some(TickClass::Transition) => ctx.request_frame_class(TickClass::Transition),
1863 Some(TickClass::CosmeticLoop) => {
1866 ctx.needs_frame = true;
1867 debug_assert!(
1868 child_ctx.paced_interval().is_some(),
1869 "CosmeticLoop frame_class with no merged paced_interval — \
1870 a new caller must be bypassing request_frame_paced_at"
1871 );
1872 ctx.absorb_paced_interval(child_ctx.paced_interval());
1873 }
1874 None => {}
1875 }
1876 if child_ctx.needs_layout() {
1877 ctx.request_layout();
1878 }
1879 let animating = child_ctx.needs_layout() || child_ctx.needs_frame();
1882 let dirtied = animating || self.painted_animating || child_ctx.layout_path_dirty;
1883 if dirtied {
1884 self.layout_dirty = true;
1885 }
1886 self.paint_dirty = dirtied;
1888 self.painted_epoch = ctx.paint_epoch;
1889 let unpaced = child_ctx.needs_layout()
1890 || (child_ctx.needs_frame() && !child_ctx.needs_frame_paced_only());
1891 let landing = self.painted_animating && !animating;
1892 if unpaced || landing || child_ctx.layout_path_dirty {
1893 ctx.layout_path_dirty = true;
1894 }
1895 self.painted_animating = animating;
1896 if let Some(ime) = child_ctx.take_ime_state() {
1897 ctx.publish_ime_state(ime);
1898 }
1899 ctx.platform_views.extend(child_ctx.take_platform_views());
1901 ctx.input_shields.extend(child_ctx.take_input_shields());
1902 }
1903
1904 pub fn semantics_child(&self, ctx: &mut SemanticsCtx) {
1907 let base = self.semantics_base(ctx);
1908 let (offset, size) = match self.transform {
1909 None => (self.origin.to_vec2(), self.size),
1910 Some(transform) => {
1911 let bounds = self
1912 .local_to_container(transform)
1913 .transform_rect_bbox(self.size.to_rect());
1914 if bounds.is_finite() {
1915 (bounds.origin().to_vec2(), bounds.size())
1916 } else {
1917 (self.origin.to_vec2(), Size::ZERO)
1919 }
1920 }
1921 };
1922 ctx.descend_into_pod(base, offset, size, |ctx| {
1923 self.widget.semantics(ctx);
1924 });
1925 }
1926
1927 fn semantics_base(&self, ctx: &mut SemanticsCtx) -> NonZeroU64 {
1929 match self.semantics_id.get() {
1930 Some(id) => id,
1931 None => {
1932 let id = ctx.alloc_base();
1933 self.semantics_id.set(Some(id));
1934 id
1935 }
1936 }
1937 }
1938
1939 pub fn event_child(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1947 if let Some(real) = crate::event::secondary_walk_event(event) {
1948 return self.walk_secondary(ctx, &real);
1949 }
1950 let local = self.localize(event);
1951 self.dispatch_local(ctx, &local)
1952 }
1953
1954 fn localize(&self, event: &InputEvent) -> InputEvent {
1956 match self.transform {
1957 None => event.translated(-self.origin.to_vec2()),
1958 Some(transform) => {
1959 match crate::hit::checked_inverse(&self.local_to_container(transform)) {
1960 Some(inverse) => event.transformed(&inverse),
1961 None => event.translated(-self.origin.to_vec2()),
1964 }
1965 }
1966 }
1967 }
1968
1969 fn walk_secondary(&mut self, ctx: &mut EventCtx, real: &InputEvent) -> EventResult {
1971 if !self.active {
1972 return EventResult::Ignored;
1973 }
1974 let local = self.localize(real);
1975 let result = if self.contacts_captor {
1976 crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local))
1977 } else {
1978 let carrier = crate::event::secondary_walk_carrier();
1979 let (_, delivered) = crate::event::run_secondary_walk(local.clone(), || {
1980 self.dispatch_local(ctx, &carrier)
1981 });
1982 match delivered {
1983 Some(result) => result,
1984 None => crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local)),
1985 }
1986 };
1987 crate::event::note_secondary_delivered(result);
1988 result
1989 }
1990
1991 fn dispatch_local(&mut self, ctx: &mut EventCtx, local: &InputEvent) -> EventResult {
1993 let hovered = self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered();
1994 let hover_eligible = ctx.is_hover_eligible() && !ctx.is_hover_claimed() && !self.active;
1997 let claim_epoch = ctx.hover_claim_epoch();
1998 let ((opted_in, opted_in_at_or_below), fold, (layout_requested, layout_path_dirty), result) = {
1999 let mut child_ctx = ctx.child_ctx(
2000 self.origin,
2001 self.size,
2002 self.focused,
2003 hovered,
2004 hover_eligible,
2005 );
2006 if let InputEvent::Shortcut(id) = local
2007 && self.takes_focus_grant(*id)
2008 {
2009 child_ctx.request_focus();
2010 }
2011 let frame = crate::event::ContactFrame::enter(self.active && self.contacts_captor);
2013 let result = self.widget.event(&mut child_ctx, local);
2014 if matches!(local, InputEvent::Shortcut(_)) && result == EventResult::Handled {
2015 crate::shortcuts::note_handled();
2016 }
2017 let (opted_in, opted_in_at_or_below) = frame.close();
2018 (
2019 (opted_in, opted_in_at_or_below),
2020 child_ctx.fold(),
2021 (child_ctx.needs_layout(), child_ctx.is_layout_path_dirty()),
2022 result,
2023 )
2024 };
2025 let (captured, hover_claimed) = (fold.captured, fold.hover_claimed);
2026 let (focus_req, focus_rel, redraw) =
2027 (fold.focus_requested, fold.focus_released, fold.needs_redraw);
2028 let path_dirty = layout_path_dirty
2031 || layout_requested
2032 || redraw
2033 || matches!(result, EventResult::Handled);
2034 if path_dirty {
2035 self.layout_dirty = true;
2036 self.paint_dirty = true;
2037 }
2038 ctx.absorb_layout(layout_requested, path_dirty);
2039 if captured {
2040 if !self.active {
2041 self.contacts_captor = false;
2043 self.contacts_path = false;
2044 }
2045 self.active = true;
2046 self.contacts_captor |= opted_in;
2047 self.contacts_path |= opted_in_at_or_below;
2048 }
2049 if hover_claimed {
2051 self.hover_epoch = claim_epoch;
2052 self.hover_root = ctx.hover_root();
2053 if HOVER_CLAIMANT.with(Cell::get).is_none() {
2055 let id = self.inspect_id().0;
2056 HOVER_CLAIMANT.with(|claimant| claimant.set(Some(id)));
2057 }
2058 }
2059 if focus_rel {
2061 self.set_focused(false);
2062 }
2063 if focus_req {
2064 if FOCUS_REQ_FROM_LIVE.with(Cell::get).is_none() {
2066 let live = self.holds_live_focus();
2067 FOCUS_REQ_FROM_LIVE.with(|c| c.set(Some(live)));
2068 }
2069 self.set_focused(true);
2070 }
2071 ctx.absorb_child(fold);
2072 result
2073 }
2074
2075 fn takes_focus_grant(&self, carrier: ShortcutId) -> bool {
2077 let Some(id) = self.inspect_id.get() else {
2078 return false;
2079 };
2080 FOCUS_GRANT.with(|grant| match grant.get() {
2081 Some((open, target, false)) if open == carrier && target == id.get() => {
2082 grant.set(Some((open, target, true)));
2083 true
2084 }
2085 _ => false,
2086 })
2087 }
2088
2089 pub fn contains(&self, point: Point) -> bool {
2092 if let Some(transform) = self.transform {
2093 return crate::hit::point_in_transformed_rect(
2094 point,
2095 self.size.to_rect(),
2096 &self.local_to_container(transform),
2097 );
2098 }
2099 point.x >= self.origin.x
2100 && point.x < self.origin.x + self.size.width
2101 && point.y >= self.origin.y
2102 && point.y < self.origin.y + self.size.height
2103 }
2104}
2105
2106#[cfg(test)]
2107mod focus_link_tests {
2108 use super::*;
2109
2110 struct Session(u64, u64, u64);
2113
2114 impl Session {
2115 fn enter(root: u64, live: u64, claim: u64) -> Self {
2116 let previous = LIVE_FOCUS_SESSION.with(|slot| slot.get());
2117 set_live_focus_session(root, live, claim);
2118 Session(previous.0, previous.1, previous.2)
2119 }
2120 }
2121
2122 impl Drop for Session {
2123 fn drop(&mut self) {
2124 set_live_focus_session(self.0, self.1, self.2);
2125 }
2126 }
2127
2128 struct Inert;
2131
2132 impl Widget for Inert {
2133 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2134 bc.constrain(Size::new(10.0, 10.0))
2135 }
2136 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2137 }
2138
2139 fn pod() -> ChildPod {
2140 ChildPod::new(Box::new(Inert))
2141 }
2142
2143 #[test]
2144 fn a_recorded_link_counts_only_while_its_stamp_names_the_live_session() {
2145 let _session = Session::enter(7, 3, 3);
2146 let mut pod = pod();
2147 pod.set_focused(true);
2148 assert!(pod.holds_live_focus(), "recorded against the live session");
2149 assert_eq!(pod.focus_epoch(), 3);
2150
2151 set_live_focus_session(7, 4, 4);
2153 assert!(
2154 pod.is_focused(),
2155 "the raw record survives, as it always did"
2156 );
2157 assert!(
2158 !pod.holds_live_focus(),
2159 "but it no longer names the session the root has"
2160 );
2161 }
2162
2163 #[test]
2164 fn a_link_of_another_root_never_counts_however_the_epochs_line_up() {
2165 let _session = Session::enter(7, 3, 3);
2166 let mut pod = pod();
2167 pod.set_focused(true);
2168
2169 set_live_focus_session(8, 3, 3);
2172 assert!(!pod.holds_live_focus());
2173 }
2174
2175 #[test]
2176 fn a_claim_recorded_during_a_dispatch_counts_before_the_dispatch_commits_it() {
2177 let _session = Session::enter(7, 3, 4);
2180 let mut claimed = pod();
2181 claimed.set_focused(true);
2182 assert_eq!(claimed.focus_epoch(), 4, "stamped with the candidate");
2183 assert!(
2184 claimed.holds_live_focus(),
2185 "and observable to the container still unwinding around the claim"
2186 );
2187
2188 let mut standing = pod();
2189 standing.focused = true;
2190 standing.focus_root = 7;
2191 standing.focus_epoch = 3;
2192 assert!(
2193 standing.holds_live_focus(),
2194 "while a link recorded before this dispatch still reads live"
2195 );
2196
2197 set_live_focus_session(7, 4, 4);
2199 assert!(claimed.holds_live_focus());
2200 assert!(!standing.holds_live_focus());
2201 }
2202
2203 #[test]
2204 fn retiring_a_link_drops_only_one_the_live_session_has_already_stranded() {
2205 let _session = Session::enter(7, 3, 3);
2206 let mut pod = pod();
2207 pod.set_focused(true);
2208 assert!(
2209 !pod.retire_stale_focus_link(),
2210 "a live link is never retired by a pass that merely walked past it"
2211 );
2212 assert!(pod.is_focused());
2213
2214 set_live_focus_session(7, 4, 4);
2215 assert!(pod.retire_stale_focus_link(), "a stranded link is dropped");
2216 assert!(
2217 !pod.is_focused(),
2218 "so the raw flag and the stamp tell one story"
2219 );
2220 assert!(
2221 !pod.retire_stale_focus_link(),
2222 "and retiring is idempotent — there is nothing left to drop"
2223 );
2224 }
2225
2226 #[test]
2227 fn a_pod_that_never_held_a_link_reports_nothing_to_retire() {
2228 let _session = Session::enter(7, 3, 3);
2229 let mut pod = pod();
2230 assert!(!pod.holds_live_focus());
2231 assert!(!pod.retire_stale_focus_link());
2232 }
2233}
2234
2235#[cfg(test)]
2236mod tests {
2237 use super::*;
2238 use crate::event::{PointerButton, PointerEvent, PointerPhase};
2239
2240 struct FixedBox {
2242 intrinsic: Size,
2243 }
2244
2245 impl Widget for FixedBox {
2246 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2247 bc.constrain(self.intrinsic)
2248 }
2249
2250 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2251 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
2252 }
2253 }
2254
2255 struct Animator;
2258
2259 impl Widget for Animator {
2260 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2261 bc.max()
2262 }
2263
2264 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2265 ctx.request_frame();
2266 }
2267 }
2268
2269 struct LayoutAnimator;
2273
2274 impl Widget for LayoutAnimator {
2275 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2276 bc.max()
2277 }
2278
2279 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2280 ctx.request_layout();
2281 }
2282 }
2283
2284 struct PacedAnimator;
2288
2289 impl Widget for PacedAnimator {
2290 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2291 bc.max()
2292 }
2293
2294 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2295 ctx.request_frame_paced();
2296 }
2297 }
2298
2299 const SLOW_PACE: Duration = Duration::from_millis(500);
2302
2303 struct SlowPacedAnimator;
2306
2307 impl Widget for SlowPacedAnimator {
2308 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2309 bc.max()
2310 }
2311
2312 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2313 ctx.request_frame_paced_at(SLOW_PACE);
2314 }
2315 }
2316
2317 #[derive(Default)]
2319 struct RecordingScene {
2320 rects: Vec<(Point, Size)>,
2321 texts: Vec<(Point, String)>,
2322 shaders: Vec<(u64, Rect, f32)>,
2323 scene_textures: Vec<(u64, Rect)>,
2324 }
2325
2326 impl PaintScene for RecordingScene {
2327 fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
2328 self.rects.push((origin, size));
2329 }
2330 fn draw_text(&mut self, origin: Point, text: &str) {
2331 self.texts.push((origin, text.to_string()));
2332 }
2333 fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
2334 self.shaders.push((program.id(), dest, time));
2335 }
2336 fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
2337 self.scene_textures.push((id, dest));
2338 }
2339 }
2340
2341 struct Probe {
2344 last_pos: Option<Point>,
2345 capture_on_down: bool,
2346 }
2347
2348 impl Widget for Probe {
2349 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2350 bc.max()
2351 }
2352 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2353 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2354 self.last_pos = Some(event.position());
2355 *ctx.state_mut::<u32>() += 1;
2356 ctx.request_redraw();
2357 if self.capture_on_down
2358 && matches!(
2359 event,
2360 InputEvent::Pointer(PointerEvent {
2361 phase: PointerPhase::Down,
2362 ..
2363 })
2364 )
2365 {
2366 ctx.capture_pointer();
2367 }
2368 EventResult::Handled
2369 }
2370 }
2371
2372 fn down(x: f64, y: f64) -> InputEvent {
2373 InputEvent::Pointer(PointerEvent {
2374 phase: PointerPhase::Down,
2375 position: Point::new(x, y),
2376 button: PointerButton::Primary,
2377 })
2378 }
2379
2380 #[derive(Default)]
2384 struct FocusProbe {
2385 saw_key: bool,
2386 had_focus_on_key: bool,
2387 }
2388
2389 impl Widget for FocusProbe {
2390 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2391 bc.max()
2392 }
2393 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2394 fn event(&mut self, ctx: &mut EventCtx, event: &crate::event::InputEvent) -> EventResult {
2395 match event {
2396 InputEvent::Pointer(PointerEvent {
2397 phase: PointerPhase::Down,
2398 ..
2399 }) => {
2400 ctx.request_focus();
2401 EventResult::Handled
2402 }
2403 InputEvent::Key(_) | InputEvent::Ime(_) => {
2404 self.saw_key = true;
2405 self.had_focus_on_key = ctx.has_focus();
2406 EventResult::Handled
2407 }
2408 _ => EventResult::Ignored,
2409 }
2410 }
2411 }
2412
2413 fn key_enter() -> InputEvent {
2414 InputEvent::Key(crate::event::KeyEvent {
2415 key: crate::event::Key::Named(crate::event::NamedKey::Enter),
2416 modifiers: crate::event::Modifiers::default(),
2417 repeat: false,
2418 })
2419 }
2420
2421 #[test]
2422 fn event_child_records_focus_on_request() {
2423 let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
2424 pod.set_origin(Point::new(5.0, 5.0));
2425 assert!(!pod.is_focused());
2426
2427 let mut count = 0u32;
2428 let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
2429 pod.event_child(&mut ctx, &down(6.0, 6.0));
2430
2431 assert!(pod.is_focused());
2434 assert!(ctx.is_focus_requested());
2435 }
2436
2437 #[test]
2438 fn event_child_threads_has_focus_into_child() {
2439 let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
2442 pod.set_focused(true);
2443
2444 let mut count = 0u32;
2445 let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
2446 pod.event_child(&mut ctx, &key_enter());
2447
2448 let probe = pod.widget_mut().downcast_mut::<FocusProbe>().unwrap();
2449 assert!(probe.saw_key);
2450 assert!(probe.had_focus_on_key);
2451 }
2452
2453 #[test]
2454 fn widget_layout_respects_constraints() {
2455 let mut w = FixedBox {
2456 intrinsic: Size::new(1000.0, 1000.0),
2457 };
2458 let mut ctx = LayoutCtx::new();
2459 let bc = BoxConstraints::loose(Size::new(200.0, 100.0));
2460 assert_eq!(w.layout(&mut ctx, &bc), Size::new(200.0, 100.0));
2461 }
2462
2463 #[test]
2464 fn with_window_insets_scopes_and_restores_on_both_contexts() {
2465 use crate::insets::EdgeInsets;
2466 let root = WindowInsets::new(EdgeInsets::new(1.0, 2.0, 3.0, 4.0), EdgeInsets::ZERO);
2467 let scoped = root.consuming(true, true, true, true);
2468
2469 let mut lctx = LayoutCtx::new();
2470 lctx.set_window_insets(root);
2471 let inside = lctx.with_window_insets(scoped, |ctx| ctx.window_insets());
2472 assert_eq!(inside, scoped);
2473 assert_eq!(lctx.window_insets(), root, "layout override restored");
2474
2475 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2476 pctx.set_window_insets(root);
2477 let inside = pctx.with_window_insets(scoped, |ctx| ctx.window_insets());
2478 assert_eq!(inside, scoped);
2479 assert_eq!(pctx.window_insets(), root, "paint override restored");
2480 }
2481
2482 #[test]
2483 fn widget_paint_emits_into_scene() {
2484 let mut w = FixedBox {
2485 intrinsic: Size::new(50.0, 20.0),
2486 };
2487 let mut scene = RecordingScene::default();
2488 let mut ctx = PaintCtx::new(Point::new(5.0, 7.0), Size::new(50.0, 20.0));
2489 w.paint(&mut ctx, &mut scene);
2490 assert_eq!(
2491 scene.rects,
2492 vec![(Point::new(5.0, 7.0), Size::new(50.0, 20.0))]
2493 );
2494 }
2495
2496 #[test]
2497 fn widget_paint_emits_shader_into_scene() {
2498 struct ShaderWidget {
2500 program: ShaderProgram,
2501 dest: Rect,
2502 time: f32,
2503 }
2504
2505 impl Widget for ShaderWidget {
2506 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2507 bc.max()
2508 }
2509
2510 fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2511 scene.draw_shader(&self.program, self.dest, self.time);
2512 }
2513 }
2514
2515 let program = ShaderProgram::new("fn main() {}");
2516 let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
2517 let time = 1.5;
2518 let mut w = ShaderWidget {
2519 program: program.clone(),
2520 dest,
2521 time,
2522 };
2523 let mut scene = RecordingScene::default();
2524 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
2525 w.paint(&mut ctx, &mut scene);
2526 assert_eq!(scene.shaders.len(), 1);
2527 assert_eq!(scene.shaders[0].0, program.id());
2528 assert_eq!(scene.shaders[0].1, dest);
2529 assert_eq!(scene.shaders[0].2, time);
2530 }
2531
2532 #[test]
2533 fn widget_paint_emits_scene_texture_into_scene() {
2534 struct SceneTextureWidget {
2536 id: u64,
2537 dest: Rect,
2538 }
2539
2540 impl Widget for SceneTextureWidget {
2541 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2542 bc.max()
2543 }
2544
2545 fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2546 scene.draw_scene_texture(self.id, self.dest);
2547 }
2548 }
2549
2550 let id = 42u64;
2551 let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
2552 let mut w = SceneTextureWidget { id, dest };
2553 let mut scene = RecordingScene::default();
2554 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
2555 w.paint(&mut ctx, &mut scene);
2556 assert_eq!(scene.scene_textures.len(), 1);
2557 assert_eq!(scene.scene_textures[0].0, id);
2558 assert_eq!(scene.scene_textures[0].1, dest);
2559 }
2560
2561 #[test]
2562 fn downcast_recovers_concrete_widget() {
2563 let mut boxed: Box<dyn Widget> = Box::new(FixedBox {
2564 intrinsic: Size::new(3.0, 4.0),
2565 });
2566 let concrete = boxed.downcast_mut::<FixedBox>().expect("downcast");
2567 assert_eq!(concrete.intrinsic, Size::new(3.0, 4.0));
2568 }
2569
2570 #[test]
2571 fn boxed_widget_delegates_every_pass() {
2572 let mut boxed: Box<dyn Widget> = Box::new(Probe {
2574 last_pos: None,
2575 capture_on_down: false,
2576 });
2577 let mut ctx = LayoutCtx::new();
2578 assert_eq!(
2579 boxed.layout(&mut ctx, &BoxConstraints::tight(Size::new(4.0, 5.0))),
2580 Size::new(4.0, 5.0)
2581 );
2582 let mut count = 0u32;
2583 let mut ectx = EventCtx::new(&mut count, Point::ZERO, Size::new(4.0, 5.0));
2584 assert_eq!(
2585 boxed.event(&mut ectx, &down(1.0, 1.0)),
2586 EventResult::Handled
2587 );
2588 assert_eq!(count, 1);
2589 }
2590
2591 #[test]
2592 fn child_pod_layout_and_paint_offset_by_origin() {
2593 let mut pod = ChildPod::new(Box::new(FixedBox {
2594 intrinsic: Size::new(30.0, 10.0),
2595 }));
2596 pod.set_origin(Point::new(12.0, 8.0));
2597
2598 let mut lctx = LayoutCtx::new();
2599 let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
2600 assert_eq!(size, Size::new(30.0, 10.0));
2601 assert_eq!(pod.size(), Size::new(30.0, 10.0));
2602
2603 let mut scene = RecordingScene::default();
2605 let mut pctx = PaintCtx::new(Point::new(100.0, 200.0), Size::new(300.0, 300.0));
2606 pod.paint_child(&mut pctx, &mut scene);
2607 assert_eq!(
2608 scene.rects,
2609 vec![(Point::new(112.0, 208.0), Size::new(30.0, 10.0))]
2610 );
2611 }
2612
2613 #[test]
2614 fn child_pod_translates_event_into_child_space() {
2615 let mut pod = ChildPod::new(Box::new(Probe {
2616 last_pos: None,
2617 capture_on_down: false,
2618 }));
2619 pod.set_origin(Point::new(10.0, 20.0));
2620
2621 let mut count = 0u32;
2622 let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(200.0, 200.0));
2623 let result = pod.event_child(&mut ctx, &down(25.0, 35.0));
2625 assert_eq!(result, EventResult::Handled);
2626 assert_eq!(count, 1); let probe = pod.widget_mut().downcast_mut::<Probe>().unwrap();
2628 assert_eq!(probe.last_pos, Some(Point::new(15.0, 15.0)));
2629 }
2630
2631 #[test]
2632 fn child_pod_propagates_capture_flag() {
2633 let mut pod = ChildPod::new(Box::new(Probe {
2634 last_pos: None,
2635 capture_on_down: true,
2636 }));
2637 pod.set_origin(Point::new(5.0, 5.0));
2638 assert!(!pod.is_active());
2639
2640 let mut count = 0u32;
2641 let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
2642 pod.event_child(&mut ctx, &down(6.0, 6.0));
2643
2644 assert!(pod.is_active());
2647 assert!(ctx.is_pointer_captured());
2648 assert!(ctx.needs_redraw());
2649 }
2650
2651 #[test]
2652 fn child_pod_bubbles_needs_frame_from_child_paint() {
2653 let mut still = ChildPod::new(Box::new(FixedBox {
2655 intrinsic: Size::new(10.0, 10.0),
2656 }));
2657 let mut lctx = LayoutCtx::new();
2658 still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2659 let mut scene = RecordingScene::default();
2660 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2661 still.paint_child(&mut pctx, &mut scene);
2662 assert!(!pctx.needs_frame());
2663
2664 let mut anim = ChildPod::new(Box::new(Animator));
2666 anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2667 let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2668 assert!(!pctx2.needs_frame());
2669 anim.paint_child(&mut pctx2, &mut scene);
2670 assert!(pctx2.needs_frame());
2671 }
2672 struct PlatformViewProbe {
2678 slot_id: u64,
2679 should_publish: bool,
2680 }
2681
2682 impl Widget for PlatformViewProbe {
2683 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2684 bc.max()
2685 }
2686
2687 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2688 if self.should_publish {
2689 ctx.publish_platform_view(PlatformViewFrame {
2690 slot_id: self.slot_id,
2691 view_type: "dev.frust.Probe".to_string(),
2692 params_json: String::new(),
2693 params_generation: 0,
2694 rect: Rect::from_origin_size(ctx.origin(), ctx.size()),
2695 clip: None,
2696 visible: true,
2697 interactive: false,
2698 shields: Vec::new(),
2699 command_index: 0,
2700 });
2701 }
2702 }
2703 }
2704
2705 #[test]
2706 fn scene_builder_command_index_reports_the_live_count() {
2707 let mut scene = frust_scene::Scene::new();
2708 {
2709 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
2710 let paint: &mut dyn PaintScene = &mut builder;
2711 assert_eq!(paint.command_index(), 0);
2712
2713 paint.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2714 assert_eq!(paint.command_index(), 1);
2715
2716 paint.push_clip(Point::ZERO, Size::new(4.0, 4.0));
2719 paint.pop_clip();
2720 assert_eq!(paint.command_index(), 3);
2721
2722 paint.push_transform(Affine::translate((1.0, 1.0)));
2724 assert_eq!(paint.command_index(), 3);
2725 paint.pop_transform();
2726 }
2727 assert_eq!(scene.commands().len(), 3);
2728 }
2729
2730 #[test]
2731 fn default_command_index_is_zero() {
2732 let mut discard = DiscardScene;
2735 discard.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2736 assert_eq!(discard.command_index(), 0);
2737
2738 let mut recording = RecordingScene::default();
2739 recording.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2740 assert_eq!(recording.command_index(), 0);
2741 }
2742
2743 #[test]
2744 fn child_pod_extends_platform_views_never_overwrites() {
2745 let mut lctx = LayoutCtx::new();
2751
2752 let mut first = ChildPod::new(Box::new(PlatformViewProbe {
2753 slot_id: 1,
2754 should_publish: true,
2755 }));
2756 first.set_origin(Point::new(0.0, 0.0));
2757 first.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2758
2759 let mut second = ChildPod::new(Box::new(PlatformViewProbe {
2760 slot_id: 2,
2761 should_publish: true,
2762 }));
2763 second.set_origin(Point::new(20.0, 0.0));
2764 second.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2765
2766 let mut scene = RecordingScene::default();
2767 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
2768 first.paint_child(&mut pctx, &mut scene);
2769 second.paint_child(&mut pctx, &mut scene);
2770
2771 let frames = pctx.take_platform_views();
2772 assert_eq!(
2773 frames.len(),
2774 2,
2775 "both slots' frames must survive, not just the last-painted one"
2776 );
2777 assert_eq!(frames[0].slot_id, 1);
2778 assert_eq!(frames[1].slot_id, 2);
2779 }
2780
2781 struct TranslatingWrapper {
2786 child: ChildPod,
2787 }
2788
2789 impl Widget for TranslatingWrapper {
2790 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2791 self.child.layout_child(ctx, bc)
2792 }
2793
2794 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2795 self.child.paint_child(ctx, scene);
2796 }
2797 }
2798
2799 #[test]
2800 fn platform_view_frame_rect_is_absolute_under_nested_translation() {
2801 let mut lctx = LayoutCtx::new();
2806
2807 let inner = ChildPod::new(Box::new(PlatformViewProbe {
2808 slot_id: 9,
2809 should_publish: true,
2810 }));
2811 let mut wrapper = TranslatingWrapper { child: inner };
2812 wrapper.child.set_origin(Point::new(5.0, 7.0));
2813 wrapper
2814 .child
2815 .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2816
2817 let mut outer = ChildPod::new(Box::new(wrapper));
2818 outer.set_origin(Point::new(100.0, 200.0));
2819 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2820
2821 let mut scene = RecordingScene::default();
2822 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
2823 outer.paint_child(&mut pctx, &mut scene);
2824
2825 let frames = pctx.take_platform_views();
2826 assert_eq!(frames.len(), 1);
2827 assert_eq!(
2828 frames[0].rect,
2829 Rect::from_origin_size(Point::new(105.0, 207.0), Size::new(10.0, 10.0))
2830 );
2831 }
2832
2833 #[test]
2834 fn paint_ctx_origin_is_absolute_through_nested_offsets() {
2835 use std::cell::Cell;
2840
2841 struct OriginRecorder {
2842 recorded_origin: Cell<Option<Point>>,
2843 }
2844
2845 impl Widget for OriginRecorder {
2846 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2847 bc.max()
2848 }
2849
2850 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2851 self.recorded_origin.set(Some(ctx.origin()));
2852 }
2853 }
2854
2855 let mut lctx = LayoutCtx::new();
2856
2857 let recorder = OriginRecorder {
2859 recorded_origin: Cell::new(None),
2860 };
2861 let inner = ChildPod::new(Box::new(recorder));
2862 let mut middle = TranslatingWrapper { child: inner };
2863 middle.child.set_origin(Point::new(20.0, 30.0));
2864 middle
2865 .child
2866 .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2867
2868 let mut outer = ChildPod::new(Box::new(middle));
2870 outer.set_origin(Point::new(50.0, 70.0));
2871 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2872
2873 let mut scene = RecordingScene::default();
2875 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
2876 outer.paint_child(&mut pctx, &mut scene);
2877
2878 let leaf = outer
2881 .widget_mut()
2882 .downcast_mut::<TranslatingWrapper>()
2883 .unwrap()
2884 .child
2885 .widget_mut()
2886 .downcast_mut::<OriginRecorder>()
2887 .unwrap();
2888 assert_eq!(
2889 leaf.recorded_origin.get(),
2890 Some(Point::new(70.0, 100.0)),
2891 "leaf must observe summed absolute origin"
2892 );
2893 }
2894
2895 #[test]
2896 fn request_frame_alone_does_not_set_needs_layout() {
2897 let mut anim = ChildPod::new(Box::new(Animator));
2900 let mut lctx = LayoutCtx::new();
2901 anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2902 let mut scene = RecordingScene::default();
2903 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2904 anim.paint_child(&mut pctx, &mut scene);
2905 assert!(pctx.needs_frame(), "request_frame sets needs_frame");
2906 assert!(
2907 !pctx.needs_layout(),
2908 "request_frame alone must NOT set needs_layout"
2909 );
2910 }
2911
2912 #[test]
2913 fn request_layout_implies_needs_frame() {
2914 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2917 assert!(!ctx.needs_frame());
2918 assert!(!ctx.needs_layout());
2919 ctx.request_layout();
2920 assert!(ctx.needs_layout());
2921 assert!(ctx.needs_frame());
2922 }
2923
2924 #[test]
2925 fn child_pod_bubbles_needs_layout_from_child_paint() {
2926 let mut still = ChildPod::new(Box::new(FixedBox {
2928 intrinsic: Size::new(10.0, 10.0),
2929 }));
2930 let mut lctx = LayoutCtx::new();
2931 still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2932 let mut scene = RecordingScene::default();
2933 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2934 still.paint_child(&mut pctx, &mut scene);
2935 assert!(!pctx.needs_layout());
2936
2937 let mut anim = ChildPod::new(Box::new(LayoutAnimator));
2939 anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2940 let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2941 assert!(!pctx2.needs_layout());
2942 anim.paint_child(&mut pctx2, &mut scene);
2943 assert!(pctx2.needs_layout());
2944 assert!(pctx2.needs_frame());
2946 }
2947
2948 #[test]
2949 fn needs_layout_bubbles_through_nested_containers() {
2950 struct SingleChildContainer {
2954 child: ChildPod,
2955 }
2956 impl Widget for SingleChildContainer {
2957 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2958 self.child.layout_child(ctx, bc)
2959 }
2960 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2961 self.child.paint_child(ctx, scene);
2962 }
2963 }
2964
2965 let inner = SingleChildContainer {
2966 child: ChildPod::new(Box::new(LayoutAnimator)),
2967 };
2968 let mut outer = ChildPod::new(Box::new(SingleChildContainer {
2969 child: ChildPod::new(Box::new(inner)),
2970 }));
2971 let mut lctx = LayoutCtx::new();
2972 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2973 let mut scene = RecordingScene::default();
2974 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2975 outer.paint_child(&mut pctx, &mut scene);
2976 assert!(
2977 pctx.needs_layout(),
2978 "needs_layout bubbles up nested containers"
2979 );
2980 assert!(pctx.needs_frame());
2981 }
2982
2983 struct SingleChildContainer {
2986 child: ChildPod,
2987 }
2988 impl Widget for SingleChildContainer {
2989 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2990 self.child.layout_child(ctx, bc)
2991 }
2992 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2993 self.child.paint_child(ctx, scene);
2994 }
2995 }
2996
2997 #[test]
2998 fn no_request_yields_no_frame_class() {
2999 let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3002 assert!(ctx.frame_class().is_none());
3003 assert!(!ctx.needs_frame());
3004 assert!(!ctx.needs_frame_paced_only());
3005 }
3006
3007 #[test]
3008 fn request_frame_is_transition_unpaced() {
3009 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3012 ctx.request_frame();
3013 assert!(ctx.needs_frame());
3014 assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
3015 assert!(
3016 !ctx.needs_frame_paced_only(),
3017 "request_frame stays unpaced (Transition), unchanged behavior"
3018 );
3019 }
3020
3021 #[test]
3022 fn request_frame_paced_is_cosmetic_loop() {
3023 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3024 ctx.request_frame_paced();
3025 assert!(ctx.needs_frame());
3026 assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3027 assert!(ctx.needs_frame_paced_only());
3028 assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
3031 }
3032
3033 #[test]
3034 fn request_frame_paced_at_carries_its_interval() {
3035 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3037 ctx.request_frame_paced_at(Duration::from_millis(500));
3038 assert!(ctx.needs_frame());
3039 assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3040 assert!(ctx.needs_frame_paced_only());
3041 assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(500)));
3042 }
3043
3044 #[test]
3045 fn request_frame_paced_at_clamps_past_the_ten_second_ceiling() {
3046 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3049 ctx.request_frame_paced_at(Duration::from_secs(10) + Duration::from_secs(1));
3050 assert_eq!(ctx.paced_interval(), Some(PaintCtx::MAX_PACED_INTERVAL));
3051 }
3052
3053 #[test]
3054 fn request_frame_paced_at_leaves_a_real_cadence_untouched() {
3055 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3059 ctx.request_frame_paced_at(Duration::from_millis(550));
3060 assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(550)));
3061 }
3062
3063 #[test]
3064 fn paced_intervals_aggregate_on_the_min_lattice() {
3065 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3069 ctx.request_frame_paced_at(Duration::from_millis(500));
3070 ctx.request_frame_paced_at(Duration::from_millis(33));
3071 assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(33)));
3072 assert!(ctx.needs_frame_paced_only());
3073
3074 let mut reversed = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3076 reversed.request_frame_paced_at(Duration::from_millis(33));
3077 reversed.request_frame_paced_at(Duration::from_millis(500));
3078 assert_eq!(reversed.paced_interval(), Some(Duration::from_millis(33)));
3079
3080 let mut shimmer_and_caret = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3083 shimmer_and_caret.request_frame_paced_at(Duration::from_millis(500));
3084 shimmer_and_caret.request_frame_paced();
3085 assert_eq!(shimmer_and_caret.paced_interval(), Some(Duration::ZERO));
3086 }
3087
3088 #[test]
3089 fn request_frame_class_cosmetic_matches_the_bare_paced_request() {
3090 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3093 ctx.request_frame_class(TickClass::CosmeticLoop);
3094 assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3095 assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
3096 }
3097
3098 #[test]
3099 fn no_paced_request_names_no_interval() {
3100 let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3103 assert_eq!(ctx.paced_interval(), None);
3104 let mut transition = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3105 transition.request_frame();
3106 assert_eq!(transition.paced_interval(), None);
3107 }
3108
3109 #[test]
3110 fn transition_dominates_cosmetic_regardless_of_order() {
3111 let mut a = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3113 a.request_frame_paced();
3114 a.request_frame();
3115 assert_eq!(a.frame_class(), Some(TickClass::Transition));
3116 assert!(!a.needs_frame_paced_only());
3117
3118 let mut b = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3120 b.request_frame();
3121 b.request_frame_paced();
3122 assert_eq!(b.frame_class(), Some(TickClass::Transition));
3123 assert!(!b.needs_frame_paced_only());
3124 }
3125
3126 #[test]
3127 fn request_layout_implies_transition_class() {
3128 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3131 ctx.request_frame_paced();
3132 assert!(ctx.needs_frame_paced_only());
3133 ctx.request_layout();
3134 assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
3135 assert!(
3136 !ctx.needs_frame_paced_only(),
3137 "request_layout implies Transition, leaving the frame unpaced"
3138 );
3139 }
3140
3141 #[test]
3142 fn child_pod_bubbles_paced_class_from_child_paint() {
3143 let mut anim = ChildPod::new(Box::new(PacedAnimator));
3146 let mut lctx = LayoutCtx::new();
3147 anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3148 let mut scene = RecordingScene::default();
3149 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3150 anim.paint_child(&mut pctx, &mut scene);
3151 assert!(pctx.needs_frame());
3152 assert_eq!(pctx.frame_class(), Some(TickClass::CosmeticLoop));
3153 assert!(pctx.needs_frame_paced_only());
3154 }
3155
3156 #[test]
3157 fn paced_class_bubbles_through_nested_containers() {
3158 let inner = SingleChildContainer {
3161 child: ChildPod::new(Box::new(PacedAnimator)),
3162 };
3163 let mut outer = ChildPod::new(Box::new(SingleChildContainer {
3164 child: ChildPod::new(Box::new(inner)),
3165 }));
3166 let mut lctx = LayoutCtx::new();
3167 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3168 let mut scene = RecordingScene::default();
3169 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3170 outer.paint_child(&mut pctx, &mut scene);
3171 assert!(pctx.needs_frame());
3172 assert!(
3173 pctx.needs_frame_paced_only(),
3174 "a nested cosmetic-loop leaf stays paced-only up the tree"
3175 );
3176 }
3177
3178 #[test]
3179 fn child_pod_bubbles_a_named_interval_unchanged() {
3180 let mut outer = ChildPod::new(Box::new(SingleChildContainer {
3184 child: ChildPod::new(Box::new(SlowPacedAnimator)),
3185 }));
3186 let mut lctx = LayoutCtx::new();
3187 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3188 let mut scene = RecordingScene::default();
3189 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3190 outer.paint_child(&mut pctx, &mut scene);
3191 assert!(pctx.needs_frame_paced_only());
3192 assert_eq!(
3193 pctx.paced_interval(),
3194 Some(SLOW_PACE),
3195 "a nested slow paced loop keeps its own cadence up the tree"
3196 );
3197 }
3198
3199 #[test]
3200 fn absorb_paced_interval_skips_on_none_without_tightening_to_zero() {
3201 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3210 ctx.absorb_paced_interval(None);
3211 assert_eq!(
3212 ctx.paced_interval(),
3213 None,
3214 "a None interval must not tighten the aggregate to Duration::ZERO"
3215 );
3216
3217 let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3219 ctx2.request_frame_paced_at(Duration::from_millis(500));
3220 ctx2.absorb_paced_interval(None);
3221 assert_eq!(
3222 ctx2.paced_interval(),
3223 Some(Duration::from_millis(500)),
3224 "a None fold must not override an already-merged interval either"
3225 );
3226 }
3227
3228 #[test]
3229 fn sibling_paced_intervals_fold_to_the_tightest() {
3230 struct TwoChildContainer {
3234 a: ChildPod,
3235 b: ChildPod,
3236 }
3237 impl Widget for TwoChildContainer {
3238 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3239 self.a.layout_child(ctx, bc);
3240 self.b.layout_child(ctx, bc)
3241 }
3242 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3243 self.a.paint_child(ctx, scene);
3244 self.b.paint_child(ctx, scene);
3245 }
3246 }
3247
3248 let mut outer = ChildPod::new(Box::new(TwoChildContainer {
3249 a: ChildPod::new(Box::new(SlowPacedAnimator)),
3250 b: ChildPod::new(Box::new(PacedAnimator)),
3251 }));
3252 let mut lctx = LayoutCtx::new();
3253 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3254 let mut scene = RecordingScene::default();
3255 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3256 outer.paint_child(&mut pctx, &mut scene);
3257 assert!(pctx.needs_frame_paced_only());
3258 assert_eq!(
3259 pctx.paced_interval(),
3260 Some(Duration::ZERO),
3261 "the tightest sibling request (the theme cap) wins the fold"
3262 );
3263 }
3264
3265 #[test]
3266 fn mixed_sibling_requests_aggregate_to_unpaced() {
3267 struct TwoChildContainer {
3270 a: ChildPod,
3271 b: ChildPod,
3272 }
3273 impl Widget for TwoChildContainer {
3274 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3275 self.a.layout_child(ctx, bc);
3276 self.b.layout_child(ctx, bc)
3277 }
3278 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3279 self.a.paint_child(ctx, scene);
3280 self.b.paint_child(ctx, scene);
3281 }
3282 }
3283
3284 let mut outer = ChildPod::new(Box::new(TwoChildContainer {
3285 a: ChildPod::new(Box::new(PacedAnimator)),
3286 b: ChildPod::new(Box::new(Animator)),
3287 }));
3288 let mut lctx = LayoutCtx::new();
3289 outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3290 let mut scene = RecordingScene::default();
3291 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3292 outer.paint_child(&mut pctx, &mut scene);
3293 assert!(pctx.needs_frame());
3294 assert_eq!(pctx.frame_class(), Some(TickClass::Transition));
3295 assert!(
3296 !pctx.needs_frame_paced_only(),
3297 "a mixed paced+transition sibling set aggregates to unpaced"
3298 );
3299 }
3300
3301 #[test]
3302 fn with_hero_registry_bubbles_paced_class() {
3303 let registry = RefCell::new(HeroFrames::default());
3306 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3307 ctx.with_hero_registry(®istry, |child| {
3308 child.request_frame_paced();
3309 });
3310 assert!(ctx.needs_frame());
3311 assert!(
3312 ctx.needs_frame_paced_only(),
3313 "with_hero_registry bubbles the paced class up"
3314 );
3315
3316 let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3318 ctx2.request_frame_paced();
3319 ctx2.with_hero_registry(®istry, |child| {
3320 child.request_frame();
3321 });
3322 assert_eq!(ctx2.frame_class(), Some(TickClass::Transition));
3323 assert!(!ctx2.needs_frame_paced_only());
3324
3325 let mut ctx3 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3328 ctx3.with_hero_registry(®istry, |child| {
3329 child.request_frame_paced_at(SLOW_PACE);
3330 });
3331 assert_eq!(ctx3.paced_interval(), Some(SLOW_PACE));
3332 ctx3.request_frame_paced();
3333 assert_eq!(
3334 ctx3.paced_interval(),
3335 Some(Duration::ZERO),
3336 "the outer theme-cap request tightens the folded aggregate"
3337 );
3338 }
3339
3340 #[test]
3341 fn child_pod_paint_seeds_has_focus_composed_with_ancestor() {
3342 use std::cell::Cell;
3343 use std::rc::Rc;
3344
3345 struct FocusProbe(Rc<Cell<Option<bool>>>);
3347 impl Widget for FocusProbe {
3348 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3349 bc.constrain(Size::new(10.0, 10.0))
3350 }
3351 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3352 self.0.set(Some(ctx.has_focus()));
3353 }
3354 }
3355
3356 let seen = Rc::new(Cell::new(None));
3357 let mut pod = ChildPod::new(Box::new(FocusProbe(seen.clone())));
3358 let mut lctx = LayoutCtx::new();
3359 pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3360 let mut scene = RecordingScene::default();
3361
3362 pod.set_focused(true);
3364 let mut focused_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3365 focused_parent.set_has_focus(true);
3366 pod.paint_child(&mut focused_parent, &mut scene);
3367 assert_eq!(seen.get(), Some(true));
3368
3369 let mut blurred_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3372 blurred_parent.set_has_focus(false);
3373 pod.paint_child(&mut blurred_parent, &mut scene);
3374 assert_eq!(seen.get(), Some(false));
3375
3376 pod.set_focused(false);
3378 let mut focused_parent2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3379 focused_parent2.set_has_focus(true);
3380 pod.paint_child(&mut focused_parent2, &mut scene);
3381 assert_eq!(seen.get(), Some(false));
3382 }
3383
3384 #[test]
3385 fn child_pod_stamps_a_hover_claim_and_seeds_it_by_epoch() {
3386 use std::cell::Cell;
3387 use std::rc::Rc;
3388
3389 struct HoverProbe {
3392 event_seen: Rc<Cell<Option<bool>>>,
3393 paint_seen: Rc<Cell<Option<bool>>>,
3394 }
3395 impl Widget for HoverProbe {
3396 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3397 bc.constrain(Size::new(10.0, 10.0))
3398 }
3399 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3400 self.paint_seen.set(Some(ctx.is_hovered()));
3401 }
3402 fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
3403 self.event_seen.set(Some(ctx.is_hovered()));
3404 ctx.claim_hover();
3405 EventResult::Ignored
3406 }
3407 }
3408
3409 let event_seen = Rc::new(Cell::new(None));
3410 let paint_seen = Rc::new(Cell::new(None));
3411 let mut pod = ChildPod::new(Box::new(HoverProbe {
3412 event_seen: event_seen.clone(),
3413 paint_seen: paint_seen.clone(),
3414 }));
3415 let mut lctx = LayoutCtx::new();
3416 pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3417 let mut scene = RecordingScene::default();
3418 let mut state = ();
3419 let event = InputEvent::Pointer(PointerEvent {
3420 phase: PointerPhase::Move,
3421 position: Point::new(5.0, 5.0),
3422 button: PointerButton::Primary,
3423 });
3424
3425 assert_eq!(pod.hover_epoch(), 0);
3428
3429 {
3431 let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3432 ctx.set_hover_epoch(4);
3433 pod.event_child(&mut ctx, &event);
3434 assert!(!ctx.is_hover_claimed(), "nothing bubbled up");
3435 }
3436 assert_eq!(pod.hover_epoch(), 0, "no stamp from an ineligible pass");
3437
3438 {
3441 let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3442 ctx.set_hover_epoch(4);
3443 ctx.set_hovered(true);
3444 ctx.set_hover_eligible(true);
3445 pod.event_child(&mut ctx, &event);
3446 assert!(ctx.is_hover_claimed());
3447 assert_eq!(
3448 event_seen.get(),
3449 Some(false),
3450 "the pod held no link going into this pass"
3451 );
3452 }
3453 assert_eq!(pod.hover_epoch(), 5);
3454
3455 let mut live = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3458 live.set_hover_epoch(5);
3459 live.set_hovered(true);
3460 pod.paint_child(&mut live, &mut scene);
3461 assert_eq!(paint_seen.get(), Some(true));
3462
3463 let mut unhovered_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3465 unhovered_parent.set_hover_epoch(5);
3466 unhovered_parent.set_hovered(false);
3467 pod.paint_child(&mut unhovered_parent, &mut scene);
3468 assert_eq!(paint_seen.get(), Some(false));
3469
3470 let mut later = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3474 later.set_hover_epoch(6);
3475 later.set_hovered(true);
3476 pod.paint_child(&mut later, &mut scene);
3477 assert_eq!(paint_seen.get(), Some(false));
3478
3479 pod.set_active(true);
3482 {
3483 let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3484 ctx.set_hover_epoch(6);
3485 ctx.set_hover_eligible(true);
3486 pod.event_child(&mut ctx, &event);
3487 assert!(!ctx.is_hover_claimed());
3488 }
3489 assert_eq!(pod.hover_epoch(), 5, "the captured pod kept its old stamp");
3490 }
3491
3492 #[test]
3500 fn only_the_first_hover_claim_in_a_pass_is_recorded() {
3501 struct Claimer;
3502 impl Widget for Claimer {
3503 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3504 bc.constrain(Size::new(10.0, 10.0))
3505 }
3506 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3507 fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
3508 ctx.claim_hover();
3509 EventResult::Ignored
3510 }
3511 }
3512
3513 let mut first = ChildPod::new(Box::new(Claimer));
3514 let mut second = ChildPod::new(Box::new(Claimer));
3515 let mut state = ();
3516 let event = InputEvent::Pointer(PointerEvent {
3517 phase: PointerPhase::Move,
3518 position: Point::new(5.0, 5.0),
3519 button: PointerButton::Primary,
3520 });
3521 let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, Size::ZERO);
3522 ctx.set_hover_epoch(2);
3523 ctx.set_hover_eligible(true);
3524
3525 first.event_child(&mut ctx, &event);
3526 second.event_child(&mut ctx, &event);
3527 assert_eq!(first.hover_epoch(), 3, "the first claim is recorded");
3528 assert_eq!(
3529 second.hover_epoch(),
3530 0,
3531 "the pass was closed to further claims"
3532 );
3533 }
3534
3535 #[test]
3536 fn child_pod_contains_uses_container_space_bounds() {
3537 let mut pod = ChildPod::new(Box::new(FixedBox {
3538 intrinsic: Size::ZERO,
3539 }));
3540 pod.set_origin(Point::new(10.0, 10.0));
3541 let mut lctx = LayoutCtx::new();
3542 pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(20.0, 20.0)));
3543
3544 assert!(pod.contains(Point::new(10.0, 10.0))); assert!(pod.contains(Point::new(29.9, 29.9)));
3546 assert!(!pod.contains(Point::new(30.0, 30.0))); assert!(!pod.contains(Point::new(9.9, 15.0)));
3548 }
3549
3550 #[derive(Default)]
3553 struct ShadowLayerRecordingScene {
3554 shadows: Vec<(Point, Size, f64, f64, Color)>,
3555 layers: Vec<(Point, Size, f32)>,
3556 pops: u32,
3557 }
3558
3559 impl PaintScene for ShadowLayerRecordingScene {
3560 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3561 fn draw_text(&mut self, _origin: Point, _text: &str) {}
3562
3563 fn draw_shadow(
3564 &mut self,
3565 origin: Point,
3566 size: Size,
3567 radius: f64,
3568 std_dev: f64,
3569 color: Color,
3570 ) {
3571 self.shadows.push((origin, size, radius, std_dev, color));
3572 }
3573
3574 fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
3575 self.layers.push((origin, size, alpha));
3576 }
3577
3578 fn pop_layer(&mut self) {
3579 self.pops += 1;
3580 }
3581 }
3582
3583 #[test]
3584 fn draw_shadow_and_push_layer_are_no_ops_when_not_overridden() {
3585 let mut scene = RecordingScene::default();
3589 scene.draw_shadow(Point::ZERO, Size::new(10.0, 10.0), 4.0, 2.0, Color::BLACK);
3590 scene.push_layer(Point::ZERO, Size::new(10.0, 10.0), 0.5);
3591 scene.pop_layer();
3592 assert!(scene.rects.is_empty());
3593 assert!(scene.texts.is_empty());
3594 }
3595
3596 #[test]
3597 fn fill_path_and_stroke_path_are_no_ops_when_not_overridden() {
3598 let mut scene = RecordingScene::default();
3601 let mut path = BezPath::new();
3602 path.move_to((0.0, 0.0));
3603 path.line_to((1.0, 0.0));
3604 scene.fill_path(Point::ZERO, &path, &Brush::Solid(Color::BLACK));
3605 scene.stroke_path(Point::ZERO, &path, 2.0, &Brush::Solid(Color::BLACK));
3606 assert!(scene.rects.is_empty());
3607 assert!(scene.texts.is_empty());
3608 }
3609
3610 #[derive(Default)]
3613 struct PathRecordingScene {
3614 fills: Vec<(Point, BezPath)>,
3615 strokes: Vec<(Point, BezPath, f64)>,
3616 }
3617
3618 impl PaintScene for PathRecordingScene {
3619 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3620 fn draw_text(&mut self, _origin: Point, _text: &str) {}
3621
3622 fn fill_path(&mut self, origin: Point, path: &BezPath, _brush: &Brush) {
3623 self.fills.push((origin, path.clone()));
3624 }
3625
3626 fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, _brush: &Brush) {
3627 self.strokes.push((origin, path.clone(), width));
3628 }
3629 }
3630
3631 #[test]
3632 fn fill_path_and_stroke_path_reach_an_overriding_implementor() {
3633 let mut scene = PathRecordingScene::default();
3634 let mut path = BezPath::new();
3635 path.move_to((0.0, 0.0));
3636 path.line_to((5.0, 0.0));
3637
3638 scene.fill_path(Point::new(1.0, 2.0), &path, &Brush::Solid(Color::BLACK));
3639 scene.stroke_path(
3640 Point::new(3.0, 4.0),
3641 &path,
3642 1.5,
3643 &Brush::Solid(Color::BLACK),
3644 );
3645
3646 assert_eq!(scene.fills.len(), 1);
3647 assert_eq!(scene.fills[0].0, Point::new(1.0, 2.0));
3648 assert_eq!(scene.fills[0].1, path);
3649
3650 assert_eq!(scene.strokes.len(), 1);
3651 assert_eq!(scene.strokes[0].0, Point::new(3.0, 4.0));
3652 assert_eq!(scene.strokes[0].1, path);
3653 assert_eq!(scene.strokes[0].2, 1.5);
3654 }
3655
3656 #[test]
3657 fn path_at_translates_path_points_by_origin() {
3658 let mut path = BezPath::new();
3659 path.move_to((0.0, 0.0));
3660 path.line_to((5.0, 0.0));
3661
3662 let translated = path_at(Point::new(10.0, 20.0), &path);
3663 let mut expected = BezPath::new();
3664 expected.move_to((10.0, 20.0));
3665 expected.line_to((15.0, 20.0));
3666 assert_eq!(translated, expected);
3667 }
3668
3669 #[derive(Debug, PartialEq)]
3673 struct TestTheme {
3674 accent: u32,
3675 }
3676
3677 #[test]
3678 fn layout_ctx_theme_as_recovers_threaded_theme() {
3679 let theme = TestTheme { accent: 7 };
3680 let ctx = LayoutCtx::new().with_theme(&theme);
3681 assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 7 }));
3682 }
3683
3684 #[test]
3685 fn layout_ctx_theme_as_is_none_without_a_theme() {
3686 let ctx = LayoutCtx::new();
3687 assert!(ctx.theme_as::<TestTheme>().is_none());
3688 }
3689
3690 #[test]
3691 fn layout_ctx_theme_as_is_none_on_type_mismatch() {
3692 let theme = TestTheme { accent: 1 };
3693 let ctx = LayoutCtx::new().with_theme(&theme);
3694 assert!(ctx.theme_as::<u32>().is_none());
3696 }
3697
3698 #[test]
3699 fn paint_ctx_theme_as_recovers_threaded_theme() {
3700 let theme = TestTheme { accent: 9 };
3701 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
3702 ctx.set_theme(Some(&theme));
3703 assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 9 }));
3704 }
3705
3706 #[test]
3707 fn paint_ctx_theme_as_is_none_without_a_theme() {
3708 let ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
3709 assert!(ctx.theme_as::<TestTheme>().is_none());
3710 }
3711
3712 #[test]
3713 fn child_pod_paint_threads_theme_into_child() {
3714 use std::cell::Cell;
3715 use std::rc::Rc;
3716
3717 struct ThemeProbe(Rc<Cell<Option<u32>>>);
3720 impl Widget for ThemeProbe {
3721 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3722 bc.constrain(Size::new(10.0, 10.0))
3723 }
3724 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3725 self.0.set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3726 }
3727 }
3728
3729 let seen = Rc::new(Cell::new(None));
3730 let mut pod = ChildPod::new(Box::new(ThemeProbe(seen.clone())));
3731 let mut lctx = LayoutCtx::new();
3732 pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3733 let mut scene = RecordingScene::default();
3734
3735 let theme = TestTheme { accent: 42 };
3737 let mut parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3738 parent.set_theme(Some(&theme));
3739 pod.paint_child(&mut parent, &mut scene);
3740 assert_eq!(seen.get(), Some(42));
3741
3742 let mut bare = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3744 pod.paint_child(&mut bare, &mut scene);
3745 assert_eq!(seen.get(), None);
3746 }
3747
3748 struct BlinkBox;
3753
3754 impl Widget for BlinkBox {
3755 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3756 bc.constrain(Size::new(4.0, 4.0))
3757 }
3758 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3759 let millis = ctx.frame_time().as_nanos() / 1_000_000;
3760 if (millis / 500).is_multiple_of(2) {
3761 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
3762 }
3763 }
3764 }
3765
3766 #[test]
3767 fn for_test_seeds_the_requested_frame_time() {
3768 let ctx = PaintCtx::for_test(Point::ZERO, Size::new(1.0, 1.0), FrameTime::from_nanos(42));
3769 assert_eq!(ctx.frame_time(), FrameTime::from_nanos(42));
3770 }
3771
3772 #[test]
3778 fn for_test_drives_a_clock_dependent_widget_to_two_appearances() {
3779 let mut widget = BlinkBox;
3780 let size = Size::new(4.0, 4.0);
3781
3782 let mut on_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(0));
3783 let mut on_scene = RecordingScene::default();
3784 widget.paint(&mut on_ctx, &mut on_scene);
3785 assert_eq!(on_scene.rects.len(), 1, "expected the caret painted on");
3786
3787 let mut off_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(500_000_000));
3788 let mut off_scene = RecordingScene::default();
3789 widget.paint(&mut off_ctx, &mut off_scene);
3790 assert!(
3791 off_scene.rects.is_empty(),
3792 "expected the caret painted off half a blink cycle later"
3793 );
3794 }
3795
3796 #[test]
3797 fn draw_shadow_and_push_layer_reach_an_overriding_implementor() {
3798 let mut scene = ShadowLayerRecordingScene::default();
3799 let origin = Point::new(3.0, 4.0);
3800 let size = Size::new(20.0, 12.0);
3801 scene.draw_shadow(origin, size, 6.0, 3.0, Color::BLACK);
3802 scene.push_layer(origin, size, 0.25);
3803 scene.pop_layer();
3804
3805 assert_eq!(scene.shadows, vec![(origin, size, 6.0, 3.0, Color::BLACK)]);
3806 assert_eq!(scene.layers, vec![(origin, size, 0.25)]);
3807 assert_eq!(scene.pops, 1);
3808 }
3809
3810 #[test]
3811 fn a_pods_type_name_resolves_through_every_box() {
3812 let single = ChildPod::new(Box::new(Animator));
3815 assert!(
3816 single.type_name().ends_with("Animator"),
3817 "{}",
3818 single.type_name()
3819 );
3820
3821 let boxed: Box<dyn Widget> = Box::new(Animator);
3822 let double = ChildPod::new(Box::new(boxed));
3823 assert!(
3824 double.type_name().ends_with("Animator"),
3825 "{}",
3826 double.type_name()
3827 );
3828 }
3829
3830 #[test]
3831 fn a_pods_tooling_id_is_assigned_once_and_never_shared() {
3832 let a = ChildPod::new(Box::new(Animator));
3833 let b = ChildPod::new(Box::new(Animator));
3834 let first = a.inspect_id();
3835 assert_eq!(a.inspect_id(), first, "a pod keeps the id it was given");
3836 assert_ne!(b.inspect_id(), first, "two pods never share an id");
3837 assert!(
3838 first.0 >= ChildPod::INSPECT_ID_BASE,
3839 "pod ids stay in the range reserved against arena WidgetIds"
3840 );
3841 }
3842
3843 #[test]
3844 fn a_leaf_visits_no_children_and_a_container_visits_all_of_its_own() {
3845 struct Two {
3846 children: Vec<ChildPod>,
3847 }
3848 impl Widget for Two {
3849 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3850 bc.max()
3851 }
3852 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3853 fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
3854 for child in &self.children {
3855 visitor(child);
3856 }
3857 }
3858 }
3859
3860 let mut seen = 0;
3862 Animator.visit_children(&mut |_| seen += 1);
3863 assert_eq!(seen, 0);
3864
3865 let container = Two {
3866 children: vec![
3867 ChildPod::new(Box::new(Animator)),
3868 ChildPod::new(Box::new(Animator)),
3869 ],
3870 };
3871 let mut ids = Vec::new();
3872 container.visit_children(&mut |child| ids.push(child.inspect_id()));
3873 assert_eq!(ids.len(), 2);
3874 assert_ne!(ids[0], ids[1]);
3875 }
3876
3877 #[derive(Default)]
3881 struct UniformOnlyScene {
3882 fills: Vec<(Point, Size, f64)>,
3883 clips: Vec<(Point, Size, f64)>,
3884 strokes: Vec<(Point, f64)>,
3885 pops: usize,
3886 }
3887
3888 impl PaintScene for UniformOnlyScene {
3889 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3890 fn draw_text(&mut self, _origin: Point, _text: &str) {}
3891 fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, _color: Color) {
3892 self.fills.push((origin, size, radius));
3893 }
3894 fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
3895 self.clips.push((origin, size, radius));
3896 }
3897 fn pop_clip(&mut self) {
3898 self.pops += 1;
3899 }
3900 fn stroke_path(&mut self, origin: Point, _path: &BezPath, width: f64, _brush: &Brush) {
3901 self.strokes.push((origin, width));
3902 }
3903 }
3904
3905 fn corner_probe_path() -> BezPath {
3906 let mut path = BezPath::new();
3907 path.move_to((0.0, 0.0));
3908 path.line_to((10.0, 0.0));
3909 path
3910 }
3911
3912 #[test]
3913 fn per_corner_paint_defaults_fall_back_to_the_uniform_methods() {
3914 let mut scene = UniformOnlyScene::default();
3918 let origin = Point::new(2.0, 3.0);
3919 let size = Size::new(20.0, 10.0);
3920 let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
3921
3922 scene.fill_rounded_rect_radii(origin, size, radii, Color::BLACK);
3923 scene.push_clip_rounded_radii(origin, size, radii);
3924 scene.pop_clip();
3925
3926 assert_eq!(scene.fills, vec![(origin, size, 12.0)]);
3927 assert_eq!(scene.clips, vec![(origin, size, 12.0)]);
3928 assert_eq!(scene.pops, 1);
3929 }
3930
3931 #[derive(Default)]
3934 struct RectClipOnlyScene {
3935 clips: Vec<(Point, Size)>,
3936 pops: usize,
3937 }
3938
3939 impl PaintScene for RectClipOnlyScene {
3940 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3941 fn draw_text(&mut self, _origin: Point, _text: &str) {}
3942 fn push_clip(&mut self, origin: Point, size: Size) {
3943 self.clips.push((origin, size));
3944 }
3945 fn pop_clip(&mut self) {
3946 self.pops += 1;
3947 }
3948 }
3949
3950 fn triangle_probe_path() -> BezPath {
3951 let mut path = BezPath::new();
3952 path.move_to((4.0, 2.0));
3953 path.line_to((14.0, 2.0));
3954 path.line_to((9.0, 12.0));
3955 path.close_path();
3956 path
3957 }
3958
3959 #[test]
3960 fn path_clip_default_pushes_the_translated_bounding_box() {
3961 let mut scene = RectClipOnlyScene::default();
3965 scene.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
3966 scene.pop_clip();
3967
3968 assert_eq!(
3969 scene.clips,
3970 vec![(Point::new(14.0, 22.0), Size::new(10.0, 10.0))]
3971 );
3972 assert_eq!(scene.pops, 1);
3973 }
3974
3975 #[test]
3976 fn scene_builder_records_a_path_clip_translated_by_origin() {
3977 let mut scene = frust_scene::Scene::new();
3978 {
3979 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
3980 let paint: &mut dyn PaintScene = &mut builder;
3981 paint.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
3982 paint.pop_clip();
3983 }
3984
3985 match &scene.commands()[0] {
3986 frust_scene::Command::PushClipPath { path, transform } => {
3987 let expected = Affine::translate((10.0, 20.0)) * triangle_probe_path();
3988 assert_eq!(*path, expected);
3989 assert_eq!(*transform, Affine::IDENTITY);
3990 }
3991 other => panic!("expected PushClipPath, got {other:?}"),
3992 }
3993 assert!(matches!(scene.commands()[1], frust_scene::Command::PopClip));
3994 }
3995
3996 #[test]
3997 fn dashed_stroke_default_falls_back_to_the_solid_stroke() {
3998 let mut scene = UniformOnlyScene::default();
3999 let origin = Point::new(5.0, 6.0);
4000 scene.stroke_path_dashed(
4001 origin,
4002 &corner_probe_path(),
4003 2.0,
4004 DashPattern::new(4.0, 2.0),
4005 &Brush::Solid(Color::BLACK),
4006 );
4007
4008 assert_eq!(scene.strokes, vec![(origin, 2.0)]);
4009 }
4010
4011 #[test]
4012 fn scene_builder_records_per_corner_radii_translated_by_origin() {
4013 let mut scene = frust_scene::Scene::new();
4017 let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
4018 {
4019 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4020 let paint: &mut dyn PaintScene = &mut builder;
4021 paint.fill_rounded_rect_radii(
4022 Point::new(2.0, 3.0),
4023 Size::new(20.0, 10.0),
4024 radii,
4025 Color::BLACK,
4026 );
4027 paint.push_clip_rounded_radii(Point::new(0.0, 0.0), Size::new(5.0, 5.0), radii);
4028 paint.pop_clip();
4029 }
4030
4031 match &scene.commands()[0] {
4032 frust_scene::Command::RoundedRect {
4033 rect, radii: got, ..
4034 } => {
4035 assert_eq!(*rect, Rect::new(2.0, 3.0, 22.0, 13.0));
4036 assert_eq!(*got, radii);
4037 }
4038 other => panic!("expected RoundedRect, got {other:?}"),
4039 }
4040 match &scene.commands()[1] {
4041 frust_scene::Command::PushClipRounded { radii: got, .. } => assert_eq!(*got, radii),
4042 other => panic!("expected PushClipRounded, got {other:?}"),
4043 }
4044 assert!(matches!(scene.commands()[2], frust_scene::Command::PopClip));
4045 }
4046
4047 #[test]
4048 fn scene_builder_records_a_dashed_stroke_translated_by_origin() {
4049 let mut scene = frust_scene::Scene::new();
4050 let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
4051 {
4052 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4053 let paint: &mut dyn PaintScene = &mut builder;
4054 paint.stroke_path_dashed(
4055 Point::new(5.0, 0.0),
4056 &corner_probe_path(),
4057 2.0,
4058 dash,
4059 &Brush::Solid(Color::BLACK),
4060 );
4061 }
4062
4063 match &scene.commands()[0] {
4064 frust_scene::Command::Path { path, style, .. } => {
4065 assert_eq!(
4066 *style,
4067 frust_scene::PathStyle::Stroke {
4068 width: 2.0,
4069 dash: Some(dash)
4070 }
4071 );
4072 assert_eq!(path.elements().len(), 2);
4074 assert!(matches!(
4075 path.elements()[0],
4076 kurbo::PathEl::MoveTo(p) if p == Point::new(5.0, 0.0)
4077 ));
4078 }
4079 other => panic!("expected Path, got {other:?}"),
4080 }
4081 }
4082
4083 #[test]
4084 fn uniform_paint_calls_still_encode_the_commands_they_always_did() {
4085 let mut scene = frust_scene::Scene::new();
4089 {
4090 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4091 let paint: &mut dyn PaintScene = &mut builder;
4092 paint.fill_rounded_rect(
4093 Point::new(0.0, 0.0),
4094 Size::new(10.0, 10.0),
4095 4.0,
4096 Color::BLACK,
4097 );
4098 paint.push_clip_rounded(Point::new(0.0, 0.0), Size::new(10.0, 10.0), 4.0);
4099 paint.pop_clip();
4100 paint.draw_shadow(
4101 Point::new(0.0, 0.0),
4102 Size::new(10.0, 10.0),
4103 4.0,
4104 2.0,
4105 Color::BLACK,
4106 );
4107 paint.stroke_path(
4108 Point::new(0.0, 0.0),
4109 &corner_probe_path(),
4110 1.0,
4111 &Brush::Solid(Color::BLACK),
4112 );
4113 }
4114
4115 let uniform = CornerRadii::uniform(4.0);
4116 match &scene.commands()[0] {
4117 frust_scene::Command::RoundedRect { radii, .. } => assert_eq!(*radii, uniform),
4118 other => panic!("expected RoundedRect, got {other:?}"),
4119 }
4120 match &scene.commands()[1] {
4121 frust_scene::Command::PushClipRounded { radii, .. } => assert_eq!(*radii, uniform),
4122 other => panic!("expected PushClipRounded, got {other:?}"),
4123 }
4124 match &scene.commands()[3] {
4125 frust_scene::Command::BlurredRoundedRect { radii, .. } => assert_eq!(*radii, uniform),
4126 other => panic!("expected BlurredRoundedRect, got {other:?}"),
4127 }
4128 match &scene.commands()[4] {
4129 frust_scene::Command::Path { style, .. } => assert_eq!(
4130 *style,
4131 frust_scene::PathStyle::Stroke {
4132 width: 1.0,
4133 dash: None
4134 }
4135 ),
4136 other => panic!("expected Path, got {other:?}"),
4137 }
4138 }
4139
4140 #[test]
4141 fn every_paint_scene_method_terminates_on_discard_scene() {
4142 use frust_scene::{FontHandle, Glyph};
4150
4151 let mut scene = DiscardScene;
4152 let origin = Point::new(2.0, 3.0);
4153 let size = Size::new(20.0, 10.0);
4154 let color = Color::BLACK;
4155 let brush = Brush::Solid(color);
4156 let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
4157 let path = corner_probe_path();
4158
4159 scene.fill_rect(origin, size, color);
4160 scene.fill_rounded_rect(origin, size, 4.0, color);
4161 scene.fill_rounded_rect_radii(origin, size, radii, color);
4162 scene.stroke_line(origin, Point::new(10.0, 10.0), 1.0, color);
4163 scene.push_clip(origin, size);
4164 scene.push_clip_rounded(origin, size, 4.0);
4165 scene.push_clip_rounded_radii(origin, size, radii);
4166 scene.pop_clip();
4167 scene.draw_text(origin, "discarded");
4168
4169 let font = FontHandle::new(peniko::FontData::new(
4170 peniko::Blob::from(Vec::<u8>::new()),
4171 0,
4172 ));
4173 let run = GlyphRun {
4174 font,
4175 font_size: 16.0,
4176 brush: brush.clone(),
4177 transform: Affine::IDENTITY,
4178 glyphs: vec![Glyph {
4179 id: 1,
4180 x: 0.0,
4181 y: 0.0,
4182 }],
4183 };
4184 scene.draw_glyph_run(run);
4185
4186 let image = peniko::ImageData {
4187 data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
4188 format: peniko::ImageFormat::Rgba8,
4189 alpha_type: peniko::ImageAlphaType::Alpha,
4190 width: 2,
4191 height: 2,
4192 };
4193 scene.draw_image(&image, Rect::new(0.0, 0.0, 20.0, 20.0));
4194
4195 let program = ShaderProgram::new("fn main() {}");
4196 scene.draw_shader(&program, Rect::new(0.0, 0.0, 10.0, 10.0), 1.5);
4197
4198 scene.draw_shadow(origin, size, 4.0, 2.0, color);
4199 scene.fill_rect_brush(origin, size, &brush);
4200 scene.fill_rounded_rect_brush(origin, size, 4.0, &brush);
4201 scene.push_layer(origin, size, 0.5);
4202 scene.pop_layer();
4203 scene.clear_rect(origin, size);
4204 scene.fill_path(origin, &path, &brush);
4205 scene.stroke_path(origin, &path, 1.0, &brush);
4206 scene.stroke_path_dashed(origin, &path, 1.0, DashPattern::new(4.0, 2.0), &brush);
4207 scene.push_transform(Affine::translate((1.0, 2.0)));
4208 scene.pop_transform();
4209 }
4210
4211 #[derive(Default)]
4215 struct MinimalRetainedRecorder {
4216 operations: Vec<String>,
4217 }
4218
4219 impl PaintScene for MinimalRetainedRecorder {
4220 fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
4221 fn draw_text(&mut self, _origin: Point, _text: &str) {}
4222 fn push_transform(&mut self, _transform: Affine) {
4223 self.operations.push("push_transform".to_string());
4224 }
4225 fn pop_transform(&mut self) {
4226 self.operations.push("pop_transform".to_string());
4227 }
4228 fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {
4229 self.operations.push("push_layer".to_string());
4230 }
4231 fn pop_layer(&mut self) {
4232 self.operations.push("pop_layer".to_string());
4233 }
4234 }
4235
4236 #[test]
4237 fn default_push_retained_pop_retained_emulates_presentation_for_recorders() {
4238 let mut recorder = MinimalRetainedRecorder::default();
4243 let origin = Point::new(10.0, 20.0);
4244 let size = Size::new(100.0, 50.0);
4245 let alpha = 0.8;
4246 let scale = 0.9;
4247
4248 recorder.push_retained(1, origin, size, alpha, scale);
4250 recorder.pop_retained();
4252
4253 assert_eq!(
4255 recorder.operations,
4256 vec![
4257 "push_transform".to_string(),
4258 "push_layer".to_string(),
4259 "pop_layer".to_string(),
4260 "pop_transform".to_string(),
4261 ]
4262 );
4263 }
4264
4265 #[test]
4266 fn scene_builder_records_push_pop_retained_commands_directly_without_extra_transforms() {
4267 let mut scene = frust_scene::Scene::new();
4273 {
4274 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4275 let paint: &mut dyn PaintScene = &mut builder;
4276
4277 paint.push_retained(7, Point::new(5.0, 10.0), Size::new(80.0, 40.0), 0.5, 1.2);
4278 paint.fill_rect(Point::new(10.0, 15.0), Size::new(20.0, 25.0), Color::BLACK);
4280 paint.pop_retained();
4281 }
4282
4283 let commands = scene.commands();
4286 assert_eq!(commands.len(), 3);
4287
4288 match &commands[0] {
4289 frust_scene::Command::PushRetained {
4290 key,
4291 rect,
4292 alpha,
4293 scale,
4294 ..
4295 } => {
4296 assert_eq!(*key, 7);
4297 assert_eq!(*rect, Rect::new(5.0, 10.0, 85.0, 50.0));
4298 assert_eq!(*alpha, 0.5);
4299 assert_eq!(*scale, 1.2);
4300 }
4301 other => panic!("expected PushRetained, got {other:?}"),
4302 }
4303
4304 match &commands[1] {
4305 frust_scene::Command::FillRect { rect, .. } => {
4306 assert_eq!(*rect, Rect::new(10.0, 15.0, 30.0, 40.0));
4307 }
4308 other => panic!("expected FillRect, got {other:?}"),
4309 }
4310
4311 assert!(
4312 matches!(commands[2], frust_scene::Command::PopRetained),
4313 "expected PopRetained, got {:?}",
4314 commands[2]
4315 );
4316 }
4317
4318 #[test]
4319 fn current_transform_is_the_scene_builders_own_and_none_for_a_recorder() {
4320 assert_eq!(
4321 MinimalRetainedRecorder::default().current_transform(),
4322 None,
4323 "a recorder tracks no transform"
4324 );
4325 let mut scene = frust_scene::Scene::new();
4326 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4327 let paint: &mut dyn PaintScene = &mut builder;
4328 assert_eq!(paint.current_transform(), Some(Affine::IDENTITY));
4329 paint.push_transform(Affine::scale(2.0));
4330 paint.push_transform(Affine::translate((3.0, 4.0)));
4331 assert_eq!(
4332 paint.current_transform(),
4333 Some(Affine::scale(2.0) * Affine::translate((3.0, 4.0))),
4334 "every open transform, composed"
4335 );
4336 }
4337
4338 #[test]
4339 fn backdrop_blur_is_a_no_op_by_default_and_recorded_by_the_scene_builder() {
4340 let mut recorder = MinimalRetainedRecorder::default();
4343 recorder.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
4344
4345 let mut scene = frust_scene::Scene::new();
4346 {
4347 let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4348 let paint: &mut dyn PaintScene = &mut builder;
4349 paint.push_transform(Affine::translate((10.0, 20.0)));
4350 paint.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
4351 }
4352 match scene.commands() {
4353 [
4354 frust_scene::Command::BackdropBlur {
4355 rect,
4356 radius,
4357 transform,
4358 },
4359 ] => {
4360 assert_eq!(*rect, Rect::new(1.0, 2.0, 4.0, 6.0));
4361 assert_eq!(*radius, 5.0);
4362 assert_eq!(*transform, Affine::translate((10.0, 20.0)));
4363 }
4364 other => panic!("expected one BackdropBlur, got {other:?}"),
4365 }
4366 }
4367
4368 #[test]
4369 fn retained_enabled_needs_the_switch_and_a_threaded_ledger() {
4370 let ledger = crate::retained::RetainedLedger::empty();
4371 assert!(
4372 !PaintCtx::new(Point::ZERO, Size::ZERO).retained_enabled(),
4373 "no ledger threaded"
4374 );
4375 let mut ctx = PaintCtx::new(Point::ZERO, Size::ZERO).with_retained_ledger(&ledger);
4376 assert!(
4377 ctx.retained_enabled(),
4378 "an empty ledger still enables brackets"
4379 );
4380 ctx.set_paint_state(0, false, Some(&ledger), 0);
4381 assert!(!ctx.retained_enabled(), "the kill switch disables them");
4382 }
4383
4384 struct Publisher;
4386
4387 impl Widget for Publisher {
4388 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4389 bc.max()
4390 }
4391 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4392 ctx.report_input_shield(Rect::new(0.0, 0.0, 1.0, 1.0));
4393 ctx.report_hero("tag", Rect::new(0.0, 0.0, 1.0, 1.0));
4394 }
4395 }
4396
4397 #[test]
4398 fn paint_publications_sum_up_through_paint_child_and_hero_registries() {
4399 let mut pod = ChildPod::new(Box::new(Publisher));
4400 pod.layout_child(
4401 &mut LayoutCtx::new(),
4402 &BoxConstraints::tight(Size::new(5.0, 5.0)),
4403 );
4404 let mut quiet = ChildPod::new(Box::new(FixedBox {
4405 intrinsic: Size::new(5.0, 5.0),
4406 }));
4407 quiet.layout_child(
4408 &mut LayoutCtx::new(),
4409 &BoxConstraints::tight(Size::new(5.0, 5.0)),
4410 );
4411
4412 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4413 quiet.paint_child(&mut ctx, &mut DiscardScene);
4414 assert_eq!(
4415 ctx.paint_publications(),
4416 0,
4417 "a plain body publishes nothing"
4418 );
4419 pod.paint_child(&mut ctx, &mut DiscardScene);
4420 assert_eq!(ctx.paint_publications(), 2, "a shield and a hero report");
4421
4422 let registry = RefCell::new(HeroFrames::new(Point::ZERO, HashMap::new()));
4423 ctx.with_hero_registry(®istry, |inner| {
4424 pod.paint_child(inner, &mut DiscardScene);
4425 });
4426 assert_eq!(
4427 ctx.paint_publications(),
4428 4,
4429 "a hero registry's subtree counts too"
4430 );
4431 }
4432}
4433
4434#[cfg(test)]
4435mod transform_tests {
4436 use std::f64::consts::FRAC_PI_4;
4437
4438 use kurbo::Vec2;
4439
4440 use super::*;
4441 use crate::event::{
4442 PointerButton, PointerEvent, PointerId, PointerPhase, ScaleEvent, ScalePhase, ScrollDelta,
4443 };
4444
4445 #[derive(Clone, Debug, PartialEq)]
4447 enum Op {
4448 Rect(Point, Size),
4449 Push(Affine),
4450 Pop,
4451 }
4452
4453 #[derive(Default)]
4456 struct OpLog(Vec<Op>);
4457
4458 impl PaintScene for OpLog {
4459 fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
4460 self.0.push(Op::Rect(origin, size));
4461 }
4462 fn draw_text(&mut self, _origin: Point, _text: &str) {}
4463 fn push_transform(&mut self, transform: Affine) {
4464 self.0.push(Op::Push(transform));
4465 }
4466 fn pop_transform(&mut self) {
4467 self.0.push(Op::Pop);
4468 }
4469 }
4470
4471 #[derive(Default)]
4474 struct Recorder {
4475 events: Vec<InputEvent>,
4476 }
4477
4478 impl Widget for Recorder {
4479 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4480 bc.max()
4481 }
4482 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4483 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
4484 }
4485 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4486 self.events.push(event.clone());
4487 if matches!(
4488 event,
4489 InputEvent::Pointer(PointerEvent {
4490 phase: PointerPhase::Down,
4491 ..
4492 })
4493 ) {
4494 ctx.capture_pointer();
4495 }
4496 EventResult::Handled
4497 }
4498 fn semantics(&self, ctx: &mut SemanticsCtx) {
4499 ctx.push_node(accesskit::Role::Label, |_| {});
4500 }
4501 }
4502
4503 struct VisibleProbe(std::rc::Rc<Cell<Option<Rect>>>);
4505
4506 impl Widget for VisibleProbe {
4507 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4508 bc.max()
4509 }
4510 fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4511 self.0.set(ctx.visible_rect());
4512 }
4513 }
4514
4515 fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
4516 InputEvent::Pointer(PointerEvent {
4517 phase,
4518 position: Point::new(x, y),
4519 button: PointerButton::Primary,
4520 })
4521 }
4522
4523 fn recorder_pod(origin: Point, size: Size) -> ChildPod {
4525 let mut pod = ChildPod::new(Box::new(Recorder::default()));
4526 pod.layout_child(&mut LayoutCtx::new(), &BoxConstraints::tight(size));
4527 pod.set_origin(origin);
4528 pod
4529 }
4530
4531 fn recorded(pod: &mut ChildPod) -> Vec<InputEvent> {
4532 pod.widget_mut()
4533 .downcast_mut::<Recorder>()
4534 .expect("recorder pod")
4535 .events
4536 .clone()
4537 }
4538
4539 fn paint(pod: &mut ChildPod, visible: Option<Rect>) -> (Vec<Op>, Option<Rect>) {
4540 let mut ctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
4541 ctx.set_visible_rect(visible);
4542 let mut scene = OpLog::default();
4543 pod.paint_child(&mut ctx, &mut scene);
4544 (scene.0, ctx.visible_rect_ref())
4545 }
4546
4547 fn dispatch(pod: &mut ChildPod, event: &InputEvent) -> EventResult {
4548 let mut state = 0u32;
4549 let mut ctx = EventCtx::new(&mut state, Point::ZERO, Size::new(400.0, 400.0));
4550 pod.event_child(&mut ctx, event)
4551 }
4552
4553 #[test]
4554 fn a_child_under_scale_and_translate_receives_down_at_its_local_point() {
4555 let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
4556 pod.set_transform(Some(
4558 Affine::translate(Vec2::new(5.0, 5.0)) * Affine::scale(2.0),
4559 ));
4560 let at = Point::new(29.0, 43.0);
4562 assert!(pod.contains(at));
4563 assert_eq!(
4564 dispatch(&mut pod, &pointer(PointerPhase::Down, at.x, at.y)),
4565 EventResult::Handled
4566 );
4567 assert!(pod.is_active(), "capture bookkeeping is unchanged");
4568 dispatch(
4571 &mut pod,
4572 &InputEvent::Scroll {
4573 position: at,
4574 delta: ScrollDelta::Pixels(0.0, 12.0),
4575 },
4576 );
4577 dispatch(
4578 &mut pod,
4579 &InputEvent::Scale(ScaleEvent {
4580 phase: ScalePhase::Update,
4581 scale_delta: 1.1,
4582 focal: at,
4583 velocity: 0.0,
4584 }),
4585 );
4586 let local = Point::new(7.0, 9.0);
4587 assert_eq!(
4588 recorded(&mut pod),
4589 vec![
4590 pointer(PointerPhase::Down, local.x, local.y),
4591 InputEvent::Scroll {
4592 position: local,
4593 delta: ScrollDelta::Pixels(0.0, 12.0),
4594 },
4595 InputEvent::Scale(ScaleEvent {
4596 phase: ScalePhase::Update,
4597 scale_delta: 1.1,
4598 focal: local,
4599 velocity: 0.0,
4600 }),
4601 ]
4602 );
4603 assert!(!pod.contains(Point::new(12.0, 22.0)));
4606 assert!(pod.contains(Point::new(100.0, 100.0)));
4607 assert!(!pod.contains(Point::new(116.0, 60.0)));
4608 let contact = InputEvent::PointerContact {
4610 pointer_id: PointerId::touch(1),
4611 event: PointerEvent {
4612 phase: PointerPhase::Move,
4613 position: at,
4614 button: PointerButton::Primary,
4615 },
4616 };
4617 dispatch(&mut pod, &contact);
4618 assert_eq!(
4619 recorded(&mut pod).last(),
4620 Some(&InputEvent::PointerContact {
4621 pointer_id: PointerId::touch(1),
4622 event: PointerEvent {
4623 phase: PointerPhase::Move,
4624 position: local,
4625 button: PointerButton::Primary,
4626 },
4627 })
4628 );
4629 }
4630
4631 #[test]
4632 fn contains_agrees_with_paint_for_rotated_content_at_the_corners() {
4633 let size = Size::new(100.0, 100.0);
4634 let mut pod = recorder_pod(Point::new(40.0, 40.0), size);
4635 pod.set_transform(Some(Affine::rotate_about(
4636 FRAC_PI_4,
4637 Point::new(50.0, 50.0),
4638 )));
4639 let (ops, _) = paint(&mut pod, None);
4640 let [Op::Push(drawn), Op::Rect(rect_origin, rect_size), Op::Pop] = ops.as_slice() else {
4641 panic!("expected push/rect/pop, got {ops:?}");
4642 };
4643 assert_eq!((*rect_origin, *rect_size), (Point::new(40.0, 40.0), size));
4644 let rect = Rect::from_origin_size(*rect_origin, *rect_size);
4645 let center = *drawn * rect.center();
4646 for corner in [
4647 Point::new(rect.x0, rect.y0),
4648 Point::new(rect.x1, rect.y0),
4649 Point::new(rect.x1, rect.y1),
4650 Point::new(rect.x0, rect.y1),
4651 ] {
4652 let painted = *drawn * corner;
4655 let inward = (center - painted).normalize();
4656 assert!(
4657 pod.contains(painted + inward),
4658 "just inside painted corner {painted:?}"
4659 );
4660 assert!(
4661 !pod.contains(painted - inward),
4662 "just outside painted corner {painted:?}"
4663 );
4664 let unrotated = corner + (rect.center() - corner) * 0.02;
4667 assert!(!pod.contains(unrotated), "AABB corner {unrotated:?}");
4668 }
4669 }
4670
4671 #[test]
4672 fn an_untransformed_pod_paints_and_routes_exactly_as_before() {
4673 let origin = Point::new(12.0, 34.0);
4674 let size = Size::new(60.0, 40.0);
4675 let events = [
4676 pointer(PointerPhase::Down, 20.0, 40.0),
4677 pointer(PointerPhase::Move, 90.0, 10.0),
4678 pointer(PointerPhase::Up, 30.0, 50.0),
4679 InputEvent::Scroll {
4680 position: Point::new(25.0, 45.0),
4681 delta: ScrollDelta::Lines(0.0, 1.0),
4682 },
4683 InputEvent::Scale(ScaleEvent {
4684 phase: ScalePhase::Begin,
4685 scale_delta: 1.0,
4686 focal: Point::new(15.0, 35.0),
4687 velocity: 0.0,
4688 }),
4689 InputEvent::Housekeeping,
4690 ];
4691 let visible = Some(Rect::new(0.0, 0.0, 50.0, 50.0));
4692
4693 let mut baseline = recorder_pod(origin, size);
4695 let baseline_paint = paint(&mut baseline, visible);
4696 for event in &events {
4697 dispatch(&mut baseline, event);
4698 }
4699 let baseline_events = recorded(&mut baseline);
4700 assert_eq!(baseline_paint.0, vec![Op::Rect(origin, size)]);
4704 assert_eq!(baseline_paint.1, visible);
4705 let translated: Vec<_> = events
4706 .iter()
4707 .map(|e| e.translated(-origin.to_vec2()))
4708 .collect();
4709 assert_eq!(baseline_events, translated);
4710
4711 let mut cleared = recorder_pod(origin, size);
4713 cleared.set_transform(Some(Affine::rotate(1.0) * Affine::scale(3.0)));
4714 cleared.set_transform(None);
4715 assert_eq!(cleared.transform(), None);
4716 assert_eq!(paint(&mut cleared, visible), baseline_paint);
4717 for event in &events {
4718 dispatch(&mut cleared, event);
4719 }
4720 assert_eq!(recorded(&mut cleared), baseline_events);
4721 for point in [
4722 Point::new(12.0, 34.0),
4723 Point::new(71.9, 73.9),
4724 Point::new(72.0, 50.0),
4725 Point::new(11.9, 50.0),
4726 ] {
4727 assert_eq!(cleared.contains(point), baseline.contains(point));
4728 }
4729 }
4730
4731 #[test]
4732 fn a_singular_transform_hits_nothing_and_never_panics() {
4733 let origin = Point::new(10.0, 10.0);
4734 for singular in [
4735 Affine::scale(0.0),
4736 Affine::scale_non_uniform(2.0, 0.0),
4737 Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
4738 Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
4739 ] {
4740 let mut pod = recorder_pod(origin, Size::new(50.0, 50.0));
4741 pod.set_transform(Some(singular));
4742 for point in [origin, Point::new(20.0, 20.0), Point::new(10.0, 30.0)] {
4743 assert!(!pod.contains(point), "{singular:?} hit {point:?}");
4744 }
4745 assert_eq!(
4747 paint(&mut pod, Some(Rect::new(0.0, 0.0, 99.0, 99.0))).0,
4748 vec![]
4749 );
4750 dispatch(&mut pod, &pointer(PointerPhase::Up, 20.0, 25.0));
4753 assert_eq!(
4754 recorded(&mut pod),
4755 vec![pointer(PointerPhase::Up, 10.0, 15.0)]
4756 );
4757 let mut sem = SemanticsCtx::new(Size::new(200.0, 200.0), 2);
4759 pod.semantics_child(&mut sem);
4760 let update = sem.finish(accesskit::NodeId(1));
4761 let bounds = update.nodes[0].1.bounds().expect("bounds");
4762 assert!(
4763 [bounds.x0, bounds.y0, bounds.x1, bounds.y1]
4764 .iter()
4765 .all(|v| v.is_finite()),
4766 "{singular:?} gave {bounds:?}"
4767 );
4768 }
4769 }
4770
4771 #[test]
4772 fn a_transformed_pod_maps_the_visible_rect_and_reports_its_bounding_box() {
4773 let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
4774 pod.set_transform(Some(Affine::scale(2.0)));
4775 let visible = Some(Rect::new(10.0, 20.0, 110.0, 120.0));
4779 let (ops, parent_visible) = paint(&mut pod, visible);
4780 assert_eq!(parent_visible, visible);
4781 assert_eq!(
4782 ops,
4783 vec![
4784 Op::Push(
4785 Affine::translate(Vec2::new(10.0, 20.0))
4786 * Affine::scale(2.0)
4787 * Affine::translate(Vec2::new(-10.0, -20.0))
4788 ),
4789 Op::Rect(Point::new(10.0, 20.0), Size::new(50.0, 50.0)),
4790 Op::Pop,
4791 ]
4792 );
4793 let seen = std::rc::Rc::new(Cell::new(None));
4796 let mut probe = ChildPod::new(Box::new(VisibleProbe(seen.clone())));
4797 probe.layout_child(
4798 &mut LayoutCtx::new(),
4799 &BoxConstraints::tight(Size::new(50.0, 50.0)),
4800 );
4801 probe.set_origin(Point::new(10.0, 20.0));
4802 probe.set_transform(Some(Affine::scale(2.0)));
4803 paint(&mut probe, visible);
4804 assert_eq!(seen.get(), Some(Rect::new(10.0, 20.0, 60.0, 70.0)));
4805 let mut sem = SemanticsCtx::new(Size::new(400.0, 400.0), 2);
4807 pod.semantics_child(&mut sem);
4808 let update = sem.finish(accesskit::NodeId(1));
4809 let bounds = update.nodes[0].1.bounds().expect("bounds");
4810 assert_eq!(
4811 (bounds.x0, bounds.y0, bounds.x1, bounds.y1),
4812 (10.0, 20.0, 110.0, 120.0)
4813 );
4814 }
4815}
4816
4817#[cfg(test)]
4818mod contact_release_tests {
4819 use super::*;
4820 use crate::event::{ContactPass, PointerButton, PointerEvent, PointerId, PointerPhase};
4821
4822 struct Grab {
4825 opt_in: bool,
4826 }
4827 impl Widget for Grab {
4828 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4829 bc.constrain(Size::new(20.0, 20.0))
4830 }
4831 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4832 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4833 if let InputEvent::Pointer(p) = event
4834 && p.phase == PointerPhase::Down
4835 {
4836 ctx.capture_pointer();
4837 if self.opt_in {
4838 ctx.capture_contacts();
4839 }
4840 }
4841 EventResult::Handled
4842 }
4843 }
4844
4845 struct Taker {
4848 inner: ChildPod,
4849 }
4850 impl Widget for Taker {
4851 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4852 self.inner.layout_child(ctx, bc);
4853 bc.constrain(Size::new(20.0, 20.0))
4854 }
4855 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4856 self.inner.paint_child(ctx, scene);
4857 }
4858 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4859 match event {
4860 InputEvent::Pointer(p)
4861 if p.phase == PointerPhase::Move && self.inner.is_active() =>
4862 {
4863 ctx.release_captured_child(&mut self.inner);
4864 EventResult::Handled
4865 }
4866 _ => self.inner.event_child(ctx, event),
4867 }
4868 }
4869 }
4870
4871 fn nested(opt_in: bool) -> ChildPod {
4873 let mut pod = ChildPod::new(Box::new(Taker {
4874 inner: ChildPod::new(Box::new(Grab { opt_in })),
4875 }));
4876 pod.layout_child(
4877 &mut LayoutCtx::new(),
4878 &BoxConstraints::tight(Size::new(20.0, 20.0)),
4879 );
4880 pod
4881 }
4882
4883 fn pointer(phase: PointerPhase) -> InputEvent {
4884 InputEvent::Pointer(PointerEvent {
4885 phase,
4886 position: Point::new(5.0, 5.0),
4887 button: PointerButton::Primary,
4888 })
4889 }
4890
4891 fn released(pod: &mut ChildPod, phase: PointerPhase) -> bool {
4894 let mut unit = ();
4895 let mut ctx = EventCtx::new(&mut unit, Point::ZERO, Size::new(20.0, 20.0));
4896 pod.event_child(&mut ctx, &pointer(phase));
4897 ctx.is_capture_released()
4898 }
4899
4900 fn inner_active(pod: &mut ChildPod) -> bool {
4901 pod.widget_mut()
4902 .downcast_mut::<Taker>()
4903 .expect("a Taker")
4904 .inner
4905 .is_active()
4906 }
4907
4908 #[test]
4909 fn releasing_the_opted_in_child_signals_and_bubbles() {
4910 let mut pod = nested(true);
4911 assert!(!released(&mut pod, PointerPhase::Down));
4912 assert!(pod.holds_contact_opt_in(), "the opt-in below is recorded");
4913 assert!(
4914 released(&mut pod, PointerPhase::Move),
4915 "and its release bubbles"
4916 );
4917 assert!(!inner_active(&mut pod), "the child left the active path");
4918 assert!(pod.is_active(), "the container that took over keeps it");
4919 }
4920
4921 #[test]
4922 fn releasing_a_child_without_the_opt_in_clears_the_link_silently() {
4923 let mut pod = nested(false);
4924 released(&mut pod, PointerPhase::Down);
4925 assert!(!pod.holds_contact_opt_in());
4926 assert!(!released(&mut pod, PointerPhase::Move));
4927 assert!(!inner_active(&mut pod));
4928 }
4929
4930 #[test]
4931 fn a_non_claimant_contact_cannot_release_anything() {
4932 let mut pod = nested(true);
4933 released(&mut pod, PointerPhase::Down);
4934 let _pass = ContactPass::enter(PointerId::touch(1), true);
4935 assert!(!released(&mut pod, PointerPhase::Move));
4936 assert!(inner_active(&mut pod), "the claimant's link stands");
4937 }
4938}