Skip to main content

snora_core/
layout.rs

1//! The application skeleton — [`AppLayout`].
2//!
3//! `AppLayout` is the **only** shape an engine consumes. It is a plain
4//! data structure with public fields plus a builder-style API. Every slot
5//! is a `Node` of the same generic type — when rendered with snora, that
6//! binds to `iced::Element<'a, Message>`, so all four layout slots accept
7//! any iced element regardless of how the application organized its view
8//! code.
9//!
10//! # Filling slots
11//!
12//! `AppLayout::new(body)` is the minimum — just a body element. Every
13//! other slot has a sensible default and is set via a chainable method:
14//!
15//! ```ignore
16//! let layout = AppLayout::new(my_body())
17//!     .header(my_header())
18//!     .side_bar(my_sidebar())
19//!     .footer(my_footer())
20//!     .direction(LayoutDirection::Rtl)
21//!     .on_close_menus(Message::CloseMenus)
22//!     .on_close_modals(Message::CloseModals);
23//! ```
24//!
25//! # Why no `PageContract`?
26//!
27//! Earlier drafts of snora required layout slots to implement a
28//! `PageContract` trait that declared `view()`, `dialog()`, `toasts()`,
29//! and close hooks. The engine never actually consumed the non-`view`
30//! methods, so users were forced to plumb them manually anyway, and the
31//! trait's associated-type machinery forced all four slots to share a
32//! single type — a painful tax that produced the `Section` enum pattern.
33//!
34//! v0.4 drops the trait. Every slot is a `Node` value of the same generic
35//! type — in practice, `iced::Element<'a, Message>`. Because any function
36//! can return an `Element`, each slot can be built by a different piece of
37//! application code without any wrapping trait or enum, and all overlay /
38//! close state lives as plain fields here.
39
40use crate::{
41    direction::LayoutDirection,
42    overlay::{BottomSheet, Dialog},
43    toast::Toast,
44};
45
46/// The complete declarative description of what should be on screen.
47///
48/// Type parameters:
49/// * `Node` — the element type your engine consumes. With the `snora`
50///   engine, this is `iced::Element<'a, Message>`.
51/// * `Message` — your application's top-level message type.
52///
53/// Fields are intentionally `pub` so that direct struct literal syntax is
54/// available for advanced callers. The `new` + chainable setters are the
55/// *canonical* path; direct construction is a power-user escape hatch.
56pub struct AppLayout<Node, Message>
57where
58    Message: Clone,
59{
60    // -----------------------------------------------------------------
61    // Primary skeleton slots.
62    // -----------------------------------------------------------------
63    /// The main content area. Required.
64    pub body: Node,
65    pub header: Option<Node>,
66    pub side_bar: Option<Node>,
67    pub footer: Option<Node>,
68
69    // -----------------------------------------------------------------
70    // Light-weight overlays (menus).
71    //
72    // These render above the skeleton but below the modal dim layer.
73    // Outside-click dismissal is wired via `on_close_menus`.
74    // -----------------------------------------------------------------
75    /// Optional header-attached dropdown (e.g. File menu's item list).
76    /// When `Some`, the engine installs a transparent backdrop that
77    /// dispatches [`Self::on_close_menus`] on any outside click.
78    pub header_menu: Option<Node>,
79    /// Optional floating context menu (right-click menu). Same backdrop
80    /// behavior as `header_menu`.
81    pub context_menu: Option<Node>,
82
83    // -----------------------------------------------------------------
84    // Modal overlays.
85    //
86    // These render above everything except toasts. The engine paints a
87    // dimmed backdrop behind them (when any modal is present) and wires
88    // outside-click to `on_close_modals`.
89    // -----------------------------------------------------------------
90    pub dialog: Option<Dialog<Node, Message>>,
91    pub bottom_sheet: Option<BottomSheet<Node, Message>>,
92
93    // -----------------------------------------------------------------
94    // Toasts.
95    //
96    // Always rendered at the top of the z-stack so they are visible even
97    // when a modal is open. Anchor position is determined by `direction`
98    // (bottom-end — i.e. bottom-right under LTR, bottom-left under RTL).
99    // -----------------------------------------------------------------
100    pub toasts: Vec<Toast<Message>>,
101
102    // -----------------------------------------------------------------
103    // Global configuration.
104    // -----------------------------------------------------------------
105    pub direction: LayoutDirection,
106
107    // -----------------------------------------------------------------
108    // Close sinks.
109    //
110    // Single source of truth for outside-click dismissal. Individual
111    // overlay values do *not* carry their own close messages — the
112    // engine dispatches through these two channels.
113    // -----------------------------------------------------------------
114    /// Dispatched when the user clicks outside an open menu (header or
115    /// context). If `None`, menus still render but the click-outside-to-
116    /// close backdrop is not installed — the application must then
117    /// provide explicit close buttons inside its menu content.
118    pub on_close_menus: Option<Message>,
119
120    /// Dispatched when the user clicks the dim backdrop of a dialog or
121    /// bottom sheet. Semantics mirror [`Self::on_close_menus`].
122    pub on_close_modals: Option<Message>,
123}
124
125impl<Node, Message> AppLayout<Node, Message>
126where
127    Message: Clone,
128{
129    /// Start a layout with only a body. All other slots default to their
130    /// empty / `None` states.
131    pub fn new(body: Node) -> Self {
132        Self {
133            body,
134            header: None,
135            side_bar: None,
136            footer: None,
137            header_menu: None,
138            context_menu: None,
139            dialog: None,
140            bottom_sheet: None,
141            toasts: Vec::new(),
142            direction: LayoutDirection::default(),
143            on_close_menus: None,
144            on_close_modals: None,
145        }
146    }
147
148    // ---------------------------------------------------------------
149    // Skeleton slot setters.
150    // ---------------------------------------------------------------
151    #[must_use]
152    pub fn header(mut self, header: Node) -> Self {
153        self.header = Some(header);
154        self
155    }
156
157    #[must_use]
158    pub fn side_bar(mut self, side_bar: Node) -> Self {
159        self.side_bar = Some(side_bar);
160        self
161    }
162
163    #[must_use]
164    pub fn footer(mut self, footer: Node) -> Self {
165        self.footer = Some(footer);
166        self
167    }
168
169    // ---------------------------------------------------------------
170    // Overlay setters.
171    // ---------------------------------------------------------------
172    #[must_use]
173    pub fn header_menu(mut self, menu: Node) -> Self {
174        self.header_menu = Some(menu);
175        self
176    }
177
178    #[must_use]
179    pub fn context_menu(mut self, menu: Node) -> Self {
180        self.context_menu = Some(menu);
181        self
182    }
183
184    #[must_use]
185    pub fn dialog(mut self, dialog: Dialog<Node, Message>) -> Self {
186        self.dialog = Some(dialog);
187        self
188    }
189
190    #[must_use]
191    pub fn bottom_sheet(mut self, sheet: BottomSheet<Node, Message>) -> Self {
192        self.bottom_sheet = Some(sheet);
193        self
194    }
195
196    #[must_use]
197    pub fn toasts(mut self, toasts: Vec<Toast<Message>>) -> Self {
198        self.toasts = toasts;
199        self
200    }
201
202    // ---------------------------------------------------------------
203    // Configuration setters.
204    // ---------------------------------------------------------------
205    #[must_use]
206    pub fn direction(mut self, direction: LayoutDirection) -> Self {
207        self.direction = direction;
208        self
209    }
210
211    #[must_use]
212    pub fn on_close_menus(mut self, msg: Message) -> Self {
213        self.on_close_menus = Some(msg);
214        self
215    }
216
217    #[must_use]
218    pub fn on_close_modals(mut self, msg: Message) -> Self {
219        self.on_close_modals = Some(msg);
220        self
221    }
222}
223
224