Skip to main content

inset_rendering/
view.rs

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