Skip to main content

inset_rendering/
view.rs

1//! Flutter counterpart: `rendering/view.dart` (`ViewConfiguration`, `RenderView`).
2//!
3//! `applyPaintTransform` and semantics wait.
4
5use std::rc::Rc;
6
7use inset_embedder::{
8    Matrix4, Offset, Rect, SceneBuilder, Size, SystemUiOverlayStyle, TargetPlatform, View, ViewRef,
9};
10use inset_foundation::{App, Handle};
11use inset_gestures::{HitTestEntry, HitTestResult, HitTestTarget, PointerEvent};
12use inset_services::SystemChrome;
13
14use crate::box_::{AnyRenderBox, BoxConstraints, BoxHitTestResult};
15use crate::layer::{ContainerLayer, ErasedLayer, OffsetLayerMixin, TransformLayer};
16use crate::object::{
17    AnyRenderObject, Constraints, RenderHandle, RenderObject, RenderObjectData, RenderObjectVTable,
18    resolve,
19};
20use crate::painting_context::PaintingContext;
21
22/// The layout constraints for the root render object.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct ViewConfiguration {
25    /// The constraints of the output surface in logical pixels.
26    pub logical_constraints: BoxConstraints,
27    /// The constraints of the output surface in physical pixels.
28    pub physical_constraints: BoxConstraints,
29    /// The pixel density of the output surface.
30    pub device_pixel_ratio: f64,
31}
32
33impl ViewConfiguration {
34    /// Creates a view configuration.
35    ///
36    /// By default, the view has zero constraints and a pixel ratio of 1.0.
37    pub const fn new(
38        physical_constraints: BoxConstraints,
39        logical_constraints: BoxConstraints,
40        device_pixel_ratio: f64,
41    ) -> ViewConfiguration {
42        ViewConfiguration {
43            logical_constraints,
44            physical_constraints,
45            device_pixel_ratio,
46        }
47    }
48
49    /// Creates a view configuration that matches `view`.
50    pub fn from_view(view: &dyn View) -> ViewConfiguration {
51        let metrics = view.metrics();
52        let physical_constraints =
53            BoxConstraints::from_view_constraints(metrics.physical_constraints);
54        let device_pixel_ratio = metrics.device_pixel_ratio;
55        ViewConfiguration {
56            logical_constraints: physical_constraints / device_pixel_ratio,
57            physical_constraints,
58            device_pixel_ratio,
59        }
60    }
61
62    /// Creates a transformation matrix that applies the
63    /// [`device_pixel_ratio`](Self::device_pixel_ratio).
64    pub fn to_matrix(&self) -> Matrix4 {
65        Matrix4::scale(
66            self.device_pixel_ratio as f32,
67            self.device_pixel_ratio as f32,
68        )
69    }
70
71    /// Returns whether [`to_matrix`](Self::to_matrix) would return a different value for
72    /// this configuration than it would for the given `old_configuration`.
73    pub fn should_update_matrix(&self, old_configuration: &ViewConfiguration) -> bool {
74        old_configuration.device_pixel_ratio != self.device_pixel_ratio
75    }
76
77    /// Transforms the provided `logical_size` to physical pixels.
78    pub fn to_physical_size(&self, logical_size: Size) -> Size {
79        self.physical_constraints
80            .constrain(logical_size * self.device_pixel_ratio)
81    }
82}
83
84impl Default for ViewConfiguration {
85    fn default() -> ViewConfiguration {
86        ViewConfiguration::new(
87            BoxConstraints::new().max_width(0.0).max_height(0.0),
88            BoxConstraints::new().max_width(0.0).max_height(0.0),
89            1.0,
90        )
91    }
92}
93
94/// The root of the render tree.
95///
96/// The view represents the total output surface of the render tree and handles
97/// bootstrapping the rendering pipeline. The view has a unique child
98/// [`crate::RenderBox`], which is required to fill the entire output surface.
99///
100/// Not a box: its constraints come from its [`configuration`](Self::configuration), and its
101/// child is the one slot Flutter's `RenderObjectWithChildMixin` gives it.
102pub struct RenderView {
103    render_object: RenderObjectData,
104    child: Option<AnyRenderBox>,
105    size: Size,
106    configuration: Option<ViewConfiguration>,
107    view: ViewRef,
108    root_transform: Option<Matrix4>,
109    /// Whether the system overlays are styled from the layer tree at every frame.
110    ///
111    /// An application that would rather drive them itself, through
112    /// [`SystemChrome::set_system_ui_overlay_style`], turns this off so its own style is
113    /// not overwritten by whatever happens to be annotated under the bars.
114    pub automatic_system_ui_adjustment: bool,
115}
116
117impl RenderView {
118    /// Creates the root of the render tree.
119    ///
120    /// Typically created by the binding (e.g., `RendererBinding`).
121    ///
122    /// Providing a `configuration` is optional, but a configuration must be set
123    /// before calling [`prepare_initial_frame`](Self::prepare_initial_frame).
124    pub fn new(
125        app: &mut App,
126        child: Option<AnyRenderBox>,
127        configuration: Option<ViewConfiguration>,
128        view: ViewRef,
129    ) -> RenderHandle<RenderView> {
130        let this = RenderHandle::from_handle(app.create(RenderView {
131            render_object: RenderObjectData::new(),
132            child: None,
133            size: Size::ZERO,
134            configuration: None,
135            view,
136            root_transform: None,
137            automatic_system_ui_adjustment: true,
138        }));
139        let data = this.render_object_data_mut(app);
140        data.object_vtable = Some(&VTABLE);
141        data.was_repaint_boundary = true;
142        data.needs_compositing = true;
143        if let Some(configuration) = configuration {
144            this.set_configuration(app, configuration);
145        }
146        this.set_child(app, child);
147        this
148    }
149
150    /// The type-erased `RenderObject` handle.
151    pub fn as_object(self: RenderHandle<Self>) -> AnyRenderObject {
152        AnyRenderObject::from_vtable(self.id(), const { &VTABLE })
153    }
154
155    /// The render object's unique child. Flutter's `RenderObjectWithChildMixin.child`.
156    pub fn child(self: RenderHandle<Self>, app: &App) -> Option<AnyRenderBox> {
157        self.get(app).child
158    }
159
160    /// Sets the unique child, adopting or dropping as Flutter's setter does.
161    pub fn set_child(self: RenderHandle<Self>, app: &mut App, value: Option<AnyRenderBox>) {
162        if let Some(old) = self.get(app).child {
163            self.as_object().drop_child(app, old.as_object());
164        }
165        self.get_mut(app).child = value;
166        if let Some(new) = value {
167            self.as_object().adopt_child(app, new.as_object());
168        }
169    }
170
171    /// The current layout size of the view.
172    pub fn size(self: RenderHandle<Self>, app: &App) -> Size {
173        self.get(app).size
174    }
175
176    /// The constraints used for the root layout.
177    ///
178    /// Typically, this configuration is set by the `RendererBinding`, when the
179    /// [`RenderView`] is registered with it. It will also update the configuration
180    /// if necessary. Therefore, if used in conjunction with the `RendererBinding`
181    /// this property must not be set manually as the `RendererBinding` will just
182    /// override it.
183    ///
184    /// # Panics
185    ///
186    /// If no configuration has been set.
187    pub fn configuration(self: RenderHandle<Self>, app: &App) -> ViewConfiguration {
188        self.get(app)
189            .configuration
190            .expect("RenderView has not been given a configuration yet")
191    }
192
193    /// Sets [`configuration`](Self::configuration).
194    pub fn set_configuration(self: RenderHandle<Self>, app: &mut App, value: ViewConfiguration) {
195        if self.get(app).configuration == Some(value) {
196            return;
197        }
198        let old_configuration = self.get(app).configuration;
199        self.get_mut(app).configuration = Some(value);
200        if self.get(app).root_transform.is_none() {
201            // [prepare_initial_frame] has not been called yet, nothing more to do for now.
202            return;
203        }
204        if old_configuration.is_none_or(|old| value.should_update_matrix(&old)) {
205            let root_layer = self.update_matrices_and_create_new_root_layer(app);
206            self.as_object()
207                .replace_root_layer(app, root_layer.as_offset_layer());
208        }
209        debug_assert!(self.get(app).root_transform.is_some());
210        self.as_object().mark_needs_layout(app);
211    }
212
213    /// Whether a [`configuration`](Self::configuration) has been set.
214    pub fn has_configuration(self: RenderHandle<Self>, app: &App) -> bool {
215        self.get(app).configuration.is_some()
216    }
217
218    /// The constraints most recently supplied for layout: the configuration's logical
219    /// constraints.
220    ///
221    /// # Panics
222    ///
223    /// If no configuration has been set.
224    pub fn constraints(self: RenderHandle<Self>, app: &App) -> BoxConstraints {
225        debug_assert!(
226            self.has_configuration(app),
227            "Constraints are not available because RenderView has not been given a configuration yet."
228        );
229        self.configuration(app).logical_constraints
230    }
231
232    /// The host view into which this render tree is rendered.
233    pub fn flutter_view(self: RenderHandle<Self>, app: &App) -> ViewRef {
234        self.get(app).view.clone()
235    }
236
237    /// Bootstrap the rendering pipeline by preparing the first frame.
238    ///
239    /// This should only be called once, and must be called before changing
240    /// [`configuration`](Self::configuration). It is typically called immediately after calling
241    /// the constructor.
242    ///
243    /// This does not actually schedule the first frame. Call
244    /// `SchedulerBinding::ensure_visual_update` for that.
245    pub fn prepare_initial_frame(self: RenderHandle<Self>, app: &mut App) {
246        debug_assert!(
247            self.as_object().owner(app).is_some(),
248            "attach the RenderView to a PipelineOwner before calling prepare_initial_frame"
249        );
250        debug_assert!(
251            self.get(app).root_transform.is_none(),
252            "prepare_initial_frame must only be called once"
253        );
254        debug_assert!(
255            self.has_configuration(app),
256            "set a configuration before calling prepare_initial_frame"
257        );
258        self.as_object().schedule_initial_layout(app);
259        let root_layer = self.update_matrices_and_create_new_root_layer(app);
260        self.as_object()
261            .schedule_initial_paint(app, root_layer.as_container_layer());
262        debug_assert!(self.get(app).root_transform.is_some());
263    }
264
265    fn update_matrices_and_create_new_root_layer(
266        self: RenderHandle<Self>,
267        app: &mut App,
268    ) -> Handle<TransformLayer> {
269        debug_assert!(self.has_configuration(app));
270        let root_transform = self.configuration(app).to_matrix();
271        self.get_mut(app).root_transform = Some(root_transform);
272        let root_layer = TransformLayer::new(app);
273        root_layer.set_transform(app, root_transform);
274        root_layer.attach(app, self.id());
275        debug_assert!(self.get(app).root_transform.is_some());
276        root_layer
277    }
278
279    /// Determines the set of render objects located at the given position.
280    ///
281    /// Returns true if the given point is contained in this render object or one of its
282    /// descendants. Adds any render objects that contain the point to the given hit test result.
283    ///
284    /// The `position` argument is in the coordinate system of the render view, which is to say,
285    /// in logical pixels. This is not necessarily the same coordinate system as that expected by
286    /// the root layer (which is in physical pixels).
287    pub fn hit_test(
288        self: RenderHandle<Self>,
289        app: &mut App,
290        result: &mut HitTestResult,
291        position: Offset,
292    ) -> bool {
293        if let Some(child) = self.child(app) {
294            child.hit_test(app, &mut BoxHitTestResult::wrap(result), position);
295        }
296        result.add(HitTestEntry::new(self));
297        true
298    }
299
300    /// Uploads the composited layer tree to the host.
301    ///
302    /// Actually causes the output of the rendering pipeline to appear on screen.
303    pub fn composite_frame(self: RenderHandle<Self>, app: &mut App) {
304        debug_assert!(
305            self.has_configuration(app),
306            "set the RenderView configuration before calling composite_frame"
307        );
308        debug_assert!(
309            self.get(app).root_transform.is_some(),
310            "call prepare_initial_frame before calling composite_frame"
311        );
312        let layer = self
313            .as_object()
314            .layer(app)
315            .expect("call prepare_initial_frame before calling composite_frame");
316        let scene = layer.build_scene(app, SceneBuilder::new());
317        if self.get(app).automatic_system_ui_adjustment {
318            self.update_system_chrome(app);
319        }
320        debug_assert!(
321            self.configuration(app)
322                .logical_constraints
323                .is_satisfied_by(self.size(app))
324        );
325        self.get(app).view.present(std::sync::Arc::new(scene));
326    }
327
328    /// Styles the system overlays from whatever the layer tree annotates beneath them.
329    ///
330    /// Takes the overlay style from the place where a system status bar and system
331    /// navigation bar are placed to update system style overlay. The center of the system
332    /// navigation bar and the center of the status bar are used to get
333    /// `SystemUiOverlayStyle`'s to update system overlay appearance.
334    ///
335    /// ```text
336    ///         Horizontal center of the screen
337    ///                 V
338    ///    ++++++++++++++++++++++++++
339    ///    |                        |
340    ///    |    System status bar   |  <- Vertical center of the status bar
341    ///    |                        |
342    ///    ++++++++++++++++++++++++++
343    ///    |                        |
344    ///    |        Content         |
345    ///    ~                        ~
346    ///    |                        |
347    ///    ++++++++++++++++++++++++++
348    ///    |                        |
349    ///    |  System navigation bar | <- Vertical center of the navigation bar
350    ///    |                        |
351    ///    ++++++++++++++++++++++++++ <- bounds.bottom
352    /// ```
353    fn update_system_chrome(self: RenderHandle<Self>, app: &mut App) {
354        let Some(layer) = self.as_object().layer(app) else {
355            return;
356        };
357        let bounds = self.paint_bounds(app);
358        let padding = self.get(app).view.metrics().padding;
359        // Center of the status bar
360        let top = Offset::new(
361            // Horizontal center of the screen
362            bounds.center().dx(),
363            // The vertical center of the system status bar. The system status bar height
364            // is kept as top window padding.
365            padding.top / 2.0,
366        );
367        // Center of the navigation bar
368        let bottom = Offset::new(
369            // Horizontal center of the screen
370            bounds.center().dx(),
371            // Vertical center of the system navigation bar. The system navigation bar
372            // height is kept as bottom window padding. The "1" needs to be subtracted from
373            // the bottom because available pixels are in (0..bottom) range. I.e. for a
374            // device with 1920 height, bound.bottom is 1920, but the most bottom drawn
375            // pixel is at 1919 position.
376            bounds.bottom - 1.0 - padding.bottom / 2.0,
377        );
378        let layer = layer.as_layer();
379        let upper = layer.find::<SystemUiOverlayStyle>(app, top);
380        // Only Android has a customizable system navigation bar.
381        let is_android = app.platform().target_platform() == TargetPlatform::Android;
382        let lower = is_android
383            .then(|| layer.find::<SystemUiOverlayStyle>(app, bottom))
384            .flatten();
385        // If there are no overlay style in the UI don't bother updating.
386        let Some(style) = merged_overlay_style(upper, lower, is_android) else {
387            return;
388        };
389        SystemChrome::set_system_ui_overlay_style(app, &style);
390    }
391
392    /// An estimate of the bounds within which this render object will paint, in physical
393    /// pixels.
394    pub fn paint_bounds(self: RenderHandle<Self>, app: &App) -> Rect {
395        Offset::ZERO & (self.size(app) * self.configuration(app).device_pixel_ratio)
396    }
397}
398
399/// The style the two sampled points make between them, and `None` where the tree annotates
400/// neither: nothing was said about the overlays, so nothing is said to the host.
401///
402/// When both are annotated the upper provides the status bar properties and the lower
403/// provides the system navigation bar properties. This is for the case where a widget at
404/// the top, an app bar say, annotates the status bar's style while another at the bottom
405/// annotates the navigation bar's. When only one of them is, it provides every property,
406/// which lets one annotated region set both.
407fn merged_overlay_style(
408    upper: Option<Rc<SystemUiOverlayStyle>>,
409    lower: Option<Rc<SystemUiOverlayStyle>>,
410    is_android: bool,
411) -> Option<SystemUiOverlayStyle> {
412    if let (Some(upper), Some(lower)) = (&upper, &lower) {
413        return Some(SystemUiOverlayStyle {
414            status_bar_brightness: upper.status_bar_brightness,
415            status_bar_icon_brightness: upper.status_bar_icon_brightness,
416            status_bar_color: upper.status_bar_color,
417            system_status_bar_contrast_enforced: upper.system_status_bar_contrast_enforced,
418            system_navigation_bar_color: lower.system_navigation_bar_color,
419            system_navigation_bar_divider_color: lower.system_navigation_bar_divider_color,
420            system_navigation_bar_icon_brightness: lower.system_navigation_bar_icon_brightness,
421            system_navigation_bar_contrast_enforced: lower.system_navigation_bar_contrast_enforced,
422        });
423    }
424    let defined = upper.or(lower)?;
425    if !is_android {
426        // Only Android has a system navigation bar to style.
427        return Some(SystemUiOverlayStyle {
428            status_bar_brightness: defined.status_bar_brightness,
429            status_bar_icon_brightness: defined.status_bar_icon_brightness,
430            status_bar_color: defined.status_bar_color,
431            system_status_bar_contrast_enforced: defined.system_status_bar_contrast_enforced,
432            ..SystemUiOverlayStyle::new()
433        });
434    }
435    Some(*defined)
436}
437
438impl RenderObject for RenderView {
439    fn render_object_data(self: RenderHandle<Self>, app: &App) -> &RenderObjectData {
440        &self.get(app).render_object
441    }
442
443    fn render_object_data_mut(self: RenderHandle<Self>, app: &mut App) -> &mut RenderObjectData {
444        &mut self.get_mut(app).render_object
445    }
446
447    fn perform_resize(self: RenderHandle<Self>, _app: &mut App) {
448        panic!("RenderView is never sized by its parent");
449    }
450
451    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
452        debug_assert!(self.get(app).root_transform.is_some());
453        let constraints = self.constraints(app);
454        let sized_by_child = !constraints.is_tight();
455        let child = self.child(app);
456        if let Some(child) = child {
457            child.layout(app, constraints, sized_by_child);
458        }
459        let size = match child {
460            Some(child) if sized_by_child => child.size(app),
461            _ => constraints.smallest(),
462        };
463        self.get_mut(app).size = size;
464        debug_assert!(size.is_finite());
465        debug_assert!(constraints.is_satisfied_by(size));
466    }
467
468    fn visit_children(
469        self: RenderHandle<Self>,
470        app: &App,
471        visitor: &mut dyn FnMut(AnyRenderObject),
472    ) {
473        if let Some(child) = self.child(app) {
474            visitor(child.as_object());
475        }
476    }
477
478    fn apply_paint_transform(
479        self: RenderHandle<Self>,
480        app: &App,
481        child: AnyRenderObject,
482        transform: &mut Matrix4,
483    ) {
484        let root_transform = self
485            .get(app)
486            .root_transform
487            .as_ref()
488            .expect("the view has a configuration");
489        crate::object::multiply(transform, root_transform);
490        debug_assert!(child.parent(app).map(AnyRenderObject::id) == Some(self.id()));
491    }
492
493    fn is_repaint_boundary(self: RenderHandle<Self>, _app: &App) -> bool {
494        true
495    }
496
497    fn paint(
498        self: RenderHandle<Self>,
499        app: &mut App,
500        context: &mut PaintingContext,
501        offset: Offset,
502    ) {
503        if let Some(child) = self.child(app) {
504            context.paint_child(app, child.as_object(), offset);
505        }
506    }
507}
508
509/// Flutter's `RenderObject.handleEvent` default: the view itself does nothing with events.
510impl HitTestTarget for RenderHandle<RenderView> {
511    fn handle_event(&self, _app: &mut App, _event: &PointerEvent, _entry: &HitTestEntry) {}
512}
513
514/// The view is its own protocol: not a box, one object, so its vtable is a plain constant.
515static VTABLE: RenderObjectVTable = RenderObjectVTable::of::<RenderView>(
516    |app, id, child| <RenderView as RenderObject>::setup_parent_data(resolve(id), app, child),
517    |app, id| RenderView::paint_bounds(resolve(id), app),
518    |app, id, child, transform| {
519        <RenderView as RenderObject>::apply_paint_transform(resolve(id), app, child, transform)
520    },
521    |app, id| <RenderView as RenderObject>::perform_resize(resolve(id), app),
522    |app, id| crate::object::RenderObjectBase::mark_needs_layout(resolve::<RenderView>(id), app),
523    None,
524    None,
525);
526
527#[cfg(test)]
528mod tests {
529    use inset_foundation::AppCell;
530    use std::cell::Cell;
531    use std::rc::Rc;
532
533    use inset_embedder::{ViewId, ViewMetrics};
534
535    use super::*;
536    use crate::box_::RenderBox;
537    use crate::layer::TransformLayer;
538    use crate::pipeline_owner::PipelineOwner;
539    use crate::proxy_box::RenderConstrainedBox;
540    use crate::shifted_box::RenderPadding;
541    use inset_painting::EdgeInsetsGeometry;
542
543    struct TestView {
544        metrics: ViewMetrics,
545        presented: Rc<Cell<u32>>,
546    }
547
548    impl View for TestView {
549        fn id(&self) -> ViewId {
550            ViewId(0)
551        }
552
553        fn metrics(&self) -> ViewMetrics {
554            self.metrics
555        }
556
557        fn present(&self, _scene: std::sync::Arc<inset_embedder::Scene>) {
558            self.presented.set(self.presented.get() + 1);
559        }
560    }
561
562    fn test_view(presented: &Rc<Cell<u32>>) -> ViewRef {
563        Rc::new(TestView {
564            metrics: ViewMetrics::default(),
565            presented: Rc::clone(presented),
566        })
567    }
568
569    /// `view_test.dart`'s `createViewConfiguration`.
570    fn create_view_configuration(size: Size, device_pixel_ratio: f64) -> ViewConfiguration {
571        let constraints = BoxConstraints::tight(size);
572        ViewConfiguration::new(
573            constraints * device_pixel_ratio,
574            constraints,
575            device_pixel_ratio,
576        )
577    }
578
579    /// `view_test.dart`: `ViewConfiguration == and hashCode`.
580    #[test]
581    fn view_configuration_equality() {
582        let a = create_view_configuration(Size::new(800.0, 600.0), 1.0);
583        let b = create_view_configuration(Size::new(800.0, 600.0), 1.0);
584        let c = create_view_configuration(Size::new(800.0, 600.0), 3.0);
585        assert_eq!(a, b);
586        assert_ne!(a, c);
587    }
588
589    /// `view_test.dart`: `Constraints are derived from configuration`.
590    #[test]
591    fn constraints_are_derived_from_configuration() {
592        let cell = AppCell::new();
593        let mut app = cell.borrow_mut();
594        let presented = Rc::new(Cell::new(0));
595        let view = RenderView::new(&mut app, None, None, test_view(&presented));
596        assert!(!view.has_configuration(&app));
597        let constraints = BoxConstraints::new()
598            .min_width(1.0)
599            .max_width(2.0)
600            .min_height(3.0)
601            .max_height(4.0);
602        view.set_configuration(
603            &mut app,
604            ViewConfiguration::new(constraints * 3.0, constraints, 3.0),
605        );
606        assert_eq!(view.constraints(&app), constraints);
607    }
608
609    /// `view_test.dart`: `Config can be set and changed after instantiation without calling
610    /// prepareInitialFrame first`.
611    #[test]
612    fn configuration_can_change_before_prepare_initial_frame() {
613        let cell = AppCell::new();
614        let mut app = cell.borrow_mut();
615        let presented = Rc::new(Cell::new(0));
616        let view = RenderView::new(&mut app, None, None, test_view(&presented));
617        view.set_configuration(
618            &mut app,
619            create_view_configuration(Size::new(100.0, 200.0), 3.0),
620        );
621        view.set_configuration(
622            &mut app,
623            create_view_configuration(Size::new(200.0, 300.0), 2.0),
624        );
625        let owner = PipelineOwner::new(&mut app, None);
626        owner.set_root_node(&mut app, Some(view.as_object()));
627        view.prepare_initial_frame(&mut app);
628    }
629
630    /// `view_test.dart`: `does not replace the root layer unnecessarily` (and when the view
631    /// resizes). Ours observes the replacement through the paint mark.
632    #[test]
633    fn does_not_replace_the_root_layer_unnecessarily() {
634        let cell = AppCell::new();
635        let mut app = cell.borrow_mut();
636        let presented = Rc::new(Cell::new(0));
637        let view = RenderView::new(
638            &mut app,
639            None,
640            Some(create_view_configuration(Size::new(100.0, 100.0), 1.0)),
641            test_view(&presented),
642        );
643        let owner = PipelineOwner::new(&mut app, None);
644        owner.set_root_node(&mut app, Some(view.as_object()));
645        view.prepare_initial_frame(&mut app);
646        owner.flush_layout(&mut app);
647        owner.flush_compositing_bits(&mut app);
648        owner.flush_paint(&mut app);
649        assert!(!view.as_object().debug_needs_paint(&app));
650
651        view.set_configuration(
652            &mut app,
653            create_view_configuration(Size::new(100.0, 1117.0), 1.0),
654        );
655        assert!(
656            !view.as_object().debug_needs_paint(&app),
657            "a resize keeps the root layer"
658        );
659
660        view.set_configuration(
661            &mut app,
662            create_view_configuration(Size::new(100.0, 1117.0), 5.0),
663        );
664        assert!(
665            view.as_object().debug_needs_paint(&app),
666            "a new ratio replaces it"
667        );
668        let layer = view.as_object().debug_layer(&app).expect("root layer");
669        let transform = app
670            .handle::<TransformLayer>(layer.id())
671            .expect("the root is a TransformLayer");
672        assert_eq!(transform.transform(&app), Some(Matrix4::scale(5.0, 5.0)));
673    }
674
675    /// `view_test.dart`: `accounts for device pixel ratio in paintBounds`, plus one frame
676    /// composited to the host.
677    #[test]
678    fn accounts_for_device_pixel_ratio_in_paint_bounds_and_presents() {
679        let cell = AppCell::new();
680        let mut app = cell.borrow_mut();
681        let presented = Rc::new(Cell::new(0));
682        let child =
683            RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(10.0, 10.0)), None);
684        let view = RenderView::new(
685            &mut app,
686            Some(child.as_box()),
687            Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
688            test_view(&presented),
689        );
690        let owner = PipelineOwner::new(&mut app, None);
691        owner.set_root_node(&mut app, Some(view.as_object()));
692        view.prepare_initial_frame(&mut app);
693        owner.flush_layout(&mut app);
694        owner.flush_compositing_bits(&mut app);
695        owner.flush_paint(&mut app);
696        view.composite_frame(&mut app);
697
698        assert_eq!(view.size(&app), Size::new(800.0, 600.0));
699        assert_eq!(
700            view.paint_bounds(&app),
701            Offset::ZERO & Size::new(1600.0, 1200.0)
702        );
703        assert_eq!(presented.get(), 1);
704    }
705
706    #[test]
707    fn coordinates_convert_through_the_paint_transforms_below_the_view() {
708        let cell = AppCell::new();
709        let mut app = cell.borrow_mut();
710        let presented = Rc::new(Cell::new(0));
711        let child =
712            RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(30.0, 40.0)), None);
713        let padding = RenderPadding::new(
714            &mut app,
715            EdgeInsetsGeometry::from_ltrb(10.0, 20.0, 0.0, 0.0),
716            None,
717            Some(child.as_box()),
718        );
719        let view = RenderView::new(
720            &mut app,
721            Some(padding.as_box()),
722            Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
723            test_view(&presented),
724        );
725        let owner = PipelineOwner::new(&mut app, None);
726        owner.set_root_node(&mut app, Some(view.as_object()));
727        view.prepare_initial_frame(&mut app);
728        owner.flush_layout(&mut app);
729
730        // The root view's own transform (logical to physical) is left out, as in Dart.
731        let child = child.as_box();
732        assert_eq!(
733            child.local_to_global(&app, Offset::ZERO, None),
734            Offset::new(10.0, 20.0)
735        );
736        assert_eq!(
737            child.global_to_local(&app, Offset::new(15.0, 25.0), None),
738            Offset::new(5.0, 5.0)
739        );
740        assert_eq!(
741            child.local_to_global(&app, Offset::new(1.0, 1.0), Some(padding.as_object())),
742            Offset::new(11.0, 21.0)
743        );
744        assert_eq!(
745            padding.as_box().global_to_local(
746                &app,
747                Offset::new(10.0, 20.0),
748                Some(child.as_object())
749            ),
750            Offset::new(20.0, 40.0)
751        );
752        assert!(
753            child
754                .as_object()
755                .paint_bounds(&app)
756                .inflate(70.0)
757                .contains(child.global_to_local(&app, Offset::new(100.0, 100.0), None))
758        );
759    }
760
761    #[test]
762    fn a_proxy_box_adds_no_paint_transform() {
763        let cell = AppCell::new();
764        let mut app = cell.borrow_mut();
765        let presented = Rc::new(Cell::new(0));
766        let child =
767            RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(30.0, 40.0)), None);
768        let proxy =
769            RenderConstrainedBox::new(&mut app, BoxConstraints::new(), Some(child.as_box()));
770        let padding = RenderPadding::new(
771            &mut app,
772            EdgeInsetsGeometry::from_ltrb(10.0, 20.0, 0.0, 0.0),
773            None,
774            Some(proxy.as_box()),
775        );
776        let view = RenderView::new(
777            &mut app,
778            Some(padding.as_box()),
779            Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
780            test_view(&presented),
781        );
782        let owner = PipelineOwner::new(&mut app, None);
783        owner.set_root_node(&mut app, Some(view.as_object()));
784        view.prepare_initial_frame(&mut app);
785        owner.flush_layout(&mut app);
786
787        assert_eq!(
788            child.as_box().local_to_global(&app, Offset::ZERO, None),
789            Offset::new(10.0, 20.0)
790        );
791        assert_eq!(
792            child
793                .as_box()
794                .global_to_local(&app, Offset::new(15.0, 25.0), None),
795            Offset::new(5.0, 5.0)
796        );
797    }
798}