Skip to main content

telar_ui_core/
window_root.rs

1//! The root a windowed application mounts.
2
3use layout_core::{AvailableSpace, LayoutError, LayoutStyle, NodeId, SizeDimension};
4use platform_core::Event;
5use ui_tree::{Component, EventResult, RenderNode};
6
7use crate::context::{compute_layout, mark_dirty, new_container};
8use crate::layout_item::LayoutItem;
9use crate::surface::{EnterMotion, IDENTITY, SurfaceTransition, apply_enter, enter_transform};
10
11/// Lays its content out against the window, because nothing above it will.
12///
13/// A percent-sized tree resolves to nothing until something hands it a definite space, and Telar hands the
14/// tree a window rather than laying it out. Without this root the content's rects stay zero and the window
15/// is black forever, which looks exactly like a renderer that never drew.
16///
17/// [`ScrollPage`](crate::ScrollPage) is the same shape for a window that is one scrolling column.
18pub struct WindowRoot {
19    root: NodeId,
20    content: Box<dyn LayoutItem>,
21    transition: Option<SurfaceTransition>,
22}
23
24impl WindowRoot {
25    /// Lays `content`'s own node out against the window, adding nothing to the tree.
26    ///
27    /// `content` must size itself to fill the window — a percent-sized box is the usual answer. For content
28    /// that sizes itself to its children instead, use [`WindowRoot::wrapping`].
29    pub fn new(content: Box<dyn LayoutItem>) -> Self {
30        Self {
31            root: content.layout_node(),
32            content,
33            transition: None,
34        }
35    }
36
37    /// Wraps `content` in a window-filling box and lays *that* out, for content that does not fill the
38    /// window on its own or that has to stretch inside a parent of a fixed size.
39    pub fn wrapping(content: Box<dyn LayoutItem>) -> Result<Self, LayoutError> {
40        let root = new_container(
41            LayoutStyle::new()
42                .flex_row()
43                .width(SizeDimension::Percent(1.0))
44                .height(SizeDimension::Percent(1.0)),
45            &[content.layout_node()],
46        )?;
47        Ok(Self {
48            root,
49            content,
50            transition: None,
51        })
52    }
53
54    pub fn animate_in(self) -> Self {
55        self.animate(SurfaceTransition::enter())
56    }
57
58    /// Drives the root from a transition the *caller* owns, so it can also send the surface back out — see
59    /// [`SurfaceTransition::leave`].
60    pub fn animate(mut self, transition: SurfaceTransition) -> Self {
61        self.transition = Some(transition);
62        self
63    }
64}
65
66impl LayoutItem for WindowRoot {
67    fn layout_node(&self) -> NodeId {
68        self.root
69    }
70}
71
72impl Component for WindowRoot {
73    fn view(&self) -> RenderNode {
74        let content = self.content.view();
75        match &self.transition {
76            Some(transition) => {
77                let (_, opacity) = enter_transform(EnterMotion::Fade, transition.get());
78                apply_enter(content, IDENTITY, opacity)
79            }
80            None => content,
81        }
82    }
83
84    /// Lays out first, then passes the resize on, so anything that has to run once the tree has real rects —
85    /// a scroll viewport that is its own layout root, a first-layout autofocus — sees it in that order.
86    ///
87    /// `Handled` regardless of what the content answered: the runner requests a redraw only for a handled
88    /// event (`runner::handler`), so reporting the content's `Ignored` would relayout and never repaint.
89    fn on_event(&mut self, event: &Event) -> EventResult {
90        if let Event::WindowResized { width, height } = event {
91            mark_dirty(self.root).ok();
92            compute_layout(
93                self.root,
94                AvailableSpace::Definite(*width as f32),
95                AvailableSpace::Definite(*height as f32),
96            )
97            .ok();
98            self.content.on_event(event);
99            return EventResult::Handled;
100        }
101        self.content.on_event(event)
102    }
103
104    fn debug_name(&self) -> &'static str {
105        "WindowRoot"
106    }
107}