smudge_core/window.rs
1//! Commands an application sends to its window, and the window state it
2//! reads.
3//!
4//! An application that draws its own title bar asks the window system to move,
5//! resize, maximize or close the window: [`start_move`] and
6//! [`start_resize`] from the pointer-down handler of the area that starts the
7//! gesture, [`toggle_maximize`] from a maximize button, [`close`] from a
8//! close button. The commands queue on the application the running code
9//! belongs to, and the windowing layer takes them with
10//! [`App::take_window_commands`](crate::App::take_window_commands) and applies
11//! them to the window. A headless application has no windowing layer, so the
12//! queue keeps the commands for a test to inspect.
13//!
14//! The windowing layer applies the commands after the input event whose
15//! handler queued them, while that event is still being handled. An
16//! interactive move or resize is only honored while the press that started it
17//! is being reported, which is why a command belongs in the pointer-down
18//! handler of the area that starts it, not in its release handler.
19//!
20//! [`maximized`] reports whether the window is maximized. It is a reactive
21//! read: a binding that reads it reruns when the windowing layer reports a
22//! new state through
23//! [`App::set_window_state`](crate::App::set_window_state).
24//!
25//! [`Cursor`] chooses the pointer's look over a node, set with the
26//! [`cursor`](crate::element::Element::cursor) builder. The cursor shows
27//! wherever the pointer is over the node, and setting one does not change
28//! which node a press or hover lands on.
29//!
30//! Code that runs with no live application above it, such as an owner whose
31//! application was dropped, queues nothing: the commands do nothing there,
32//! and [`maximized`] reads `false`.
33//!
34//! On the web target the windowing layer applies no commands and reports the
35//! state as never maximized, so these functions queue and read without
36//! effect. That rests on winit's web backend, which refuses the move and
37//! resize calls and treats maximize as a no-op through winit 0.30; a winit
38//! upgrade that changes this changes the behavior here.
39//!
40//! # Examples
41//!
42//! A title bar area that moves the window while it is pressed, and a binding
43//! that shows a restore hint while the window is maximized:
44//!
45//! ```
46//! use smudge_core::geometry::Color;
47//! use smudge_core::style::px;
48//! use smudge_core::widget::panel;
49//! use smudge_core::window;
50//!
51//! let header = panel()
52//! .width(px(320.0))
53//! .height(px(36.0))
54//! .background(Color::rgb(24, 26, 32))
55//! .on_pointer_down(|_| window::start_move())
56//! .child(panel().height(px(8.0)).hidden(|| window::maximized()));
57//! ```
58
59use std::cell::RefCell;
60use std::rc::{Rc, Weak};
61
62use crate::reactive::{Owner, RwSignal, prelude::*, runtime};
63
64/// The edge or corner of a window that an interactive resize pulls.
65#[derive(Copy, Clone, Debug, PartialEq, Eq)]
66pub enum ResizeEdge {
67 /// The top edge.
68 North,
69 /// The bottom edge.
70 South,
71 /// The right edge.
72 East,
73 /// The left edge.
74 West,
75 /// The top right corner.
76 NorthEast,
77 /// The top left corner.
78 NorthWest,
79 /// The bottom right corner.
80 SouthEast,
81 /// The bottom left corner.
82 SouthWest,
83}
84
85/// One request to change the window, as
86/// [`App::take_window_commands`](crate::App::take_window_commands) hands it
87/// to the windowing layer.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum WindowCommand {
91 /// Start an interactive move, as if the user grabbed the window's title
92 /// bar with the press that is being handled.
93 Move,
94 /// Start an interactive resize from an edge or corner, as if the user
95 /// grabbed that edge with the press that is being handled.
96 Resize(ResizeEdge),
97 /// Restore the window if it is maximized, or maximize it if it is not.
98 ToggleMaximize,
99 /// Close the window and end the run, as a close button does.
100 Close,
101}
102
103/// The window state the windowing layer reports to the application.
104///
105/// More fields may follow, so a state is built from the default and the
106/// `with_` methods rather than as a struct literal:
107///
108/// ```
109/// use smudge_core::window::WindowState;
110///
111/// let state = WindowState::default().with_maximized(true);
112/// assert!(state.maximized);
113/// ```
114#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
115#[non_exhaustive]
116pub struct WindowState {
117 /// Whether the window is maximized.
118 pub maximized: bool,
119}
120
121impl WindowState {
122 /// Returns this state with its maximized flag set to `maximized`.
123 pub fn with_maximized(mut self, maximized: bool) -> WindowState {
124 self.maximized = maximized;
125 self
126 }
127}
128
129/// The look of the pointer over a node.
130///
131/// The windowing layer shows the cursor of the deepest enabled node under the
132/// pointer that sets one, and the platform default when none does. The resize
133/// variants name the direction the resize would pull, so an edge handle
134/// shows the same look the window system uses when it resizes that edge.
135#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
136#[non_exhaustive]
137pub enum Cursor {
138 /// The platform default, usually an arrow.
139 #[default]
140 Default,
141 /// A clickable target, usually a pointing hand.
142 Pointer,
143 /// A place where text can be selected or a caret put, usually an I
144 /// beam.
145 Text,
146 /// A window edge or corner that resizes in that direction.
147 Resize(ResizeEdge),
148}
149
150/// Queues a command to start an interactive move of the window.
151///
152/// Call it from the pointer-down handler of the area that starts the move,
153/// because the window system honors the request only while the press that
154/// started it is being handled. With no application above the running code
155/// the call does nothing.
156pub fn start_move() {
157 queue(WindowCommand::Move);
158}
159
160/// Queues a command to start an interactive resize of the window from
161/// `edge`.
162///
163/// Call it from the pointer-down handler of the handle that starts the
164/// resize, because the window system honors the request only while the press
165/// that started it is being handled. With no application above the running
166/// code the call does nothing.
167pub fn start_resize(edge: ResizeEdge) {
168 queue(WindowCommand::Resize(edge));
169}
170
171/// Queues a command to maximize the window, or to restore it when it is
172/// already maximized.
173///
174/// With no application above the running code the call does nothing.
175pub fn toggle_maximize() {
176 queue(WindowCommand::ToggleMaximize);
177}
178
179/// Queues a command to close the window and end the run.
180///
181/// With no application above the running code the call does nothing.
182pub fn close() {
183 queue(WindowCommand::Close);
184}
185
186/// Returns whether the window is maximized, as the windowing layer last
187/// reported it.
188///
189/// This is a reactive read: a binding that calls it reruns when the state
190/// changes, including a rerun or a memo recomputation that runs with no
191/// owner current. A windowing layer that reports nothing, such as a headless
192/// test, leaves it at `false`, and so does code with no live application
193/// above it.
194pub fn maximized() -> bool {
195 let Some(book) = running_book() else {
196 return false;
197 };
198 book.borrow().maximized.get()
199}
200
201/// Appends `command` to the command queue of the application the running
202/// code belongs to, or drops it when no live application is above the code.
203fn queue(command: WindowCommand) {
204 if let Some(book) = running_book() {
205 book.borrow_mut().commands.push(command);
206 }
207}
208
209/// Returns the window book of the application the running code belongs to.
210///
211/// A running effect or memo names its application through the owner it was
212/// created under, because its reruns run with no owner current. Other code,
213/// such as an input handler, names it through the current owner.
214fn running_book() -> Option<Rc<RefCell<WindowBook>>> {
215 book_of(&runtime::running_owner()?)
216}
217
218/// Replaces the window state the application's [`maximized`] reads, but only
219/// when it changed, so an equal report wakes no binding.
220pub(crate) fn set_state(book: &Rc<RefCell<WindowBook>>, state: WindowState) {
221 let maximized = book.borrow().maximized.clone();
222 if maximized.get() != state.maximized {
223 maximized.set(state.maximized);
224 }
225}
226
227/// Takes every command queued so far, in the order they were queued.
228pub(crate) fn take_commands(book: &Rc<RefCell<WindowBook>>) -> Vec<WindowCommand> {
229 std::mem::take(&mut book.borrow_mut().commands)
230}
231
232/// Creates the book of a new application, whose [`maximized`] signal is
233/// created under the application's root owner so it dies with it.
234pub(crate) fn new_book(root: &Owner) -> Rc<RefCell<WindowBook>> {
235 let maximized = root.with(|| RwSignal::new(false));
236 Rc::new(RefCell::new(WindowBook {
237 commands: Vec::new(),
238 maximized,
239 }))
240}
241
242/// The queued commands and the window state of one application.
243///
244/// The application owns the book. Window commands reach it from any owner
245/// under the application's root, and the windowing layer drains it and feeds
246/// the state signal back.
247pub(crate) struct WindowBook {
248 /// The commands queued since the windowing layer last took them.
249 commands: Vec<WindowCommand>,
250 /// The state the windowing layer last reported.
251 maximized: RwSignal<bool>,
252}
253
254/// Returns the window book of the application `owner` belongs to, walking up
255/// the owner chain to the root that holds it.
256fn book_of(owner: &Rc<runtime::OwnerData>) -> Option<Rc<RefCell<WindowBook>>> {
257 let mut current = Rc::clone(owner);
258 loop {
259 let book = current
260 .window_book
261 .borrow()
262 .as_ref()
263 .and_then(Weak::upgrade);
264 if book.is_some() {
265 return book;
266 }
267 let parent = current.parent.as_ref()?.upgrade()?;
268 current = parent;
269 }
270}