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}