Skip to main content

zest_widget/widget/
window.rs

1//! Titled panel: a title bar (filled rect + title text, optional close
2//! button) above a content area holding one child.
3//!
4//! `Window` is a *compound* widget — internally it composes a [`Column`] of a
5//! title-bar [`Row`] (title [`Text`] plus an optional close [`Button`]) above
6//! a content [`Container`] wrapping the supplied child.
7//! The internal tree is (re)built in [`arrange`](Widget::arrange) from the
8//! window's own fields, then the measure / touch / draw protocol is
9//! forwarded to it, so the host only deals with `Window` itself.
10//!
11//! The close button (present only when [`Window::on_close`] is set) emits
12//! the host message on release, exactly like a plain
13//! [`Button`].
14
15use super::{
16    Widget,
17    button::Button,
18    column::Column,
19    container::Container,
20    element::{Element, IntoElement},
21    row::Row,
22    text::Text,
23};
24use alloc::string::String;
25use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
26use zest_core::{Constraints, Horizontal, Length, RenderError, Renderer, TouchPhase, Vertical};
27use zest_theme::Theme;
28
29/// Height of the title bar in pixels.
30const TITLE_BAR_H: u32 = 28;
31
32/// Titled panel with a title bar and a single content child.
33pub struct Window<'a, C: PixelColor, M: Clone> {
34    rect: Rectangle,
35    title: String,
36    on_close: Option<M>,
37    child: Option<Element<'a, C, M>>,
38    padding: u32,
39    width: Length,
40    height: Length,
41    /// The composed internal widget tree, rebuilt each `arrange`.
42    tree: Option<Element<'a, C, M>>,
43}
44
45impl<'a, C: PixelColor + 'a, M: Clone + 'a> Window<'a, C, M> {
46    /// Create a new empty window. The parent assigns position and size
47    /// via `arrange`; use `.width(...)` / `.height(...)` to constrain the
48    /// slot.
49    pub fn new() -> Self {
50        Self {
51            rect: Rectangle::zero(),
52            title: String::new(),
53            on_close: None,
54            child: None,
55            padding: 6,
56            width: Length::Fill,
57            height: Length::Fill,
58            tree: None,
59        }
60    }
61
62    /// Set the title bar text.
63    #[must_use]
64    pub fn title(mut self, title: impl Into<String>) -> Self {
65        self.title = title.into();
66        self
67    }
68
69    /// Show a close button in the title bar that emits `msg` on
70    /// release. Omitting this hides the close button entirely.
71    #[must_use]
72    pub fn on_close(mut self, msg: M) -> Self {
73        self.on_close = Some(msg);
74        self
75    }
76
77    /// Set the content child placed below the title bar.
78    #[must_use]
79    pub fn child<W>(mut self, child: W) -> Self
80    where
81        W: Widget<C, M> + 'a,
82    {
83        self.child = Some(Element::new(child));
84        self
85    }
86
87    /// Inner padding around the content child.
88    #[must_use]
89    pub fn padding(mut self, padding: u32) -> Self {
90        self.padding = padding;
91        self
92    }
93
94    /// Width sizing intent.
95    #[must_use]
96    pub fn width(mut self, width: impl Into<Length>) -> Self {
97        self.width = width.into();
98        self
99    }
100
101    /// Height sizing intent.
102    #[must_use]
103    pub fn height(mut self, height: impl Into<Length>) -> Self {
104        self.height = height.into();
105        self
106    }
107
108    /// Build the internal Column tree from the current fields, consuming
109    /// the title string, the close message, and the child element.
110    fn build_tree(&mut self) -> Element<'a, C, M> {
111        // Title bar: a Row with the title (filling) and an optional close
112        // button pinned to the right.
113        let title = Text::new(self.title.clone())
114            .align_x(Horizontal::Left)
115            .align_y(Vertical::Center)
116            .width(Length::Fill)
117            .height(Length::Fill);
118
119        let mut bar: Row<'a, C, M> = Row::new()
120            .spacing(4)
121            .height(Length::Fixed(TITLE_BAR_H))
122            .push(title);
123
124        if let Some(msg) = self.on_close.clone() {
125            bar = bar.push(
126                Button::new("x")
127                    .on_press(msg)
128                    .width(Length::Fixed(TITLE_BAR_H.saturating_sub(4)))
129                    .height(Length::Fill),
130            );
131        }
132
133        // Content area: a Container wrapping the child (if any), padded.
134        let mut content: Container<'a, C, M> =
135            Container::new().padding(self.padding).height(Length::Fill);
136        if let Some(child) = self.child.take() {
137            content = content.child(child);
138        }
139
140        Column::new()
141            .spacing(0)
142            .width(self.width)
143            .height(self.height)
144            .push(bar)
145            .push(content)
146            .into_element()
147    }
148}
149
150impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Window<'a, C, M> {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Window<'a, C, M> {
157    fn measure(&mut self, constraints: Constraints) -> Size {
158        if self.tree.is_none() {
159            self.tree = Some(self.build_tree());
160        }
161        let w = self
162            .width
163            .resolve(constraints.max.width, constraints.max.width);
164        let h = self
165            .height
166            .resolve(constraints.max.height, constraints.max.height);
167        let size = constraints.clamp(Size::new(w, h));
168        if let Some(tree) = self.tree.as_mut() {
169            tree.measure(Constraints::loose(size));
170        }
171        size
172    }
173
174    fn preferred_size(&self) -> (Length, Length) {
175        (self.width, self.height)
176    }
177
178    fn arrange(&mut self, rect: Rectangle) {
179        self.rect = rect;
180        if self.tree.is_none() {
181            self.tree = Some(self.build_tree());
182        }
183        if let Some(tree) = self.tree.as_mut() {
184            tree.arrange(rect);
185        }
186    }
187
188    fn rect(&self) -> Rectangle {
189        self.rect
190    }
191
192    fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
193        self.tree
194            .as_mut()
195            .and_then(|tree| tree.handle_touch(point, phase))
196    }
197
198    fn mark_pressed(&mut self, point: Point) {
199        if let Some(tree) = self.tree.as_mut() {
200            tree.mark_pressed(point);
201        }
202    }
203
204    fn draw<'t>(
205        &self,
206        renderer: &mut dyn Renderer<C>,
207        theme: &Theme<'t, C>,
208    ) -> Result<(), RenderError> {
209        // Panel background + title-bar fill are drawn directly so the
210        // window reads as a framed surface regardless of the child.
211        renderer.fill_rect(self.rect, theme.primary.base)?;
212        let bar = Rectangle::new(
213            self.rect.top_left,
214            Size::new(self.rect.size.width, TITLE_BAR_H.min(self.rect.size.height)),
215        );
216        renderer.fill_rect(bar, theme.secondary.base)?;
217        renderer.stroke_rect(self.rect, theme.background.divider)?;
218
219        if let Some(tree) = &self.tree {
220            tree.draw(renderer, theme)?;
221        }
222        Ok(())
223    }
224}