Skip to main content

crabshell_ui/
floating.rs

1//! [`FloatingWindow`], an ordinary window that the compositor places and
2//! the user can move, and what a widget in one can ask of it.
3
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use crabshell_reactive::{Signal, untrack};
8
9use crate::cx::{Anchor, Cx};
10use crate::input::{Handler, run_isolated};
11use crate::prop::{IntoProp, Prop};
12use crate::region::Region;
13use crate::style::Style;
14use crate::tree::{Tree, borrow};
15use crate::widgets::Widget;
16use crate::window::{Anchors, Hints, Output, Window};
17
18pub(crate) type CloseHandler = Rc<Handler<dyn Fn(&Cx)>>;
19
20/// A window like an application's, which the compositor places, tiles or
21/// floats: a launcher, a settings panel, a picture viewer. Show one with
22/// `cx.floating` from the `crabshell` crate. Every property takes a value
23/// or a closure.
24///
25/// ```
26/// use crabshell_ui::{FloatingWindow, Text};
27///
28/// let about = FloatingWindow::new()
29///     .title("About")
30///     .width(320)
31///     .height(200)
32///     .child(Text::new("hello"));
33/// ```
34pub struct FloatingWindow {
35  pub(crate) title: Option<Prop<String>>,
36  pub(crate) width: Option<Prop<u32>>,
37  pub(crate) height: Option<Prop<u32>>,
38  pub(crate) min_size: Option<Prop<(u32, u32)>>,
39  pub(crate) max_size: Option<Prop<(u32, u32)>>,
40  pub(crate) maximized: Option<Prop<bool>>,
41  pub(crate) fullscreen: Option<Prop<bool>>,
42  pub(crate) visible: Option<Prop<bool>>,
43  pub(crate) hints: Hints,
44  pub(crate) on_close: Option<CloseHandler>,
45  pub(crate) style: Option<Prop<Style>>,
46  pub(crate) child: Option<Box<dyn Widget>>,
47}
48
49impl Default for FloatingWindow {
50  fn default() -> FloatingWindow {
51    FloatingWindow::new()
52  }
53}
54
55impl FloatingWindow {
56  /// The default width and height in logical pixels.
57  pub const DEFAULT_SIZE: (u32, u32) = (400, 300);
58
59  /// A visible, empty window of the default size.
60  pub fn new() -> FloatingWindow {
61    FloatingWindow {
62      title: None,
63      width: None,
64      height: None,
65      min_size: None,
66      max_size: None,
67      maximized: None,
68      fullscreen: None,
69      visible: None,
70      hints: Hints::default(),
71      on_close: None,
72      style: None,
73      child: None,
74    }
75  }
76
77  /// Sets the title the compositor shows, as in a title bar or task list.
78  pub fn title(mut self, title: impl IntoProp<String>) -> FloatingWindow {
79    self.title = Some(title.into_prop());
80    self
81  }
82
83  /// Sets the width it asks for, in logical pixels. The compositor may
84  /// give it another, as when tiling it.
85  pub fn width(mut self, width: impl IntoProp<u32>) -> FloatingWindow {
86    self.width = Some(width.into_prop());
87    self
88  }
89
90  /// Sets the height it asks for, in logical pixels.
91  pub fn height(mut self, height: impl IntoProp<u32>) -> FloatingWindow {
92    self.height = Some(height.into_prop());
93    self
94  }
95
96  /// Sets the smallest size the user may make it, as `(width, height)`;
97  /// 0 leaves a side free.
98  pub fn min_size(mut self, size: impl IntoProp<(u32, u32)>) -> FloatingWindow {
99    self.min_size = Some(size.into_prop());
100    self
101  }
102
103  /// Sets the largest size the user may make it, as `(width, height)`;
104  /// 0 leaves a side free.
105  pub fn max_size(mut self, size: impl IntoProp<(u32, u32)>) -> FloatingWindow {
106    self.max_size = Some(size.into_prop());
107    self
108  }
109
110  /// Sets whether it asks to fill the screen but for panels.
111  pub fn maximized(mut self, maximized: impl IntoProp<bool>) -> FloatingWindow {
112    self.maximized = Some(maximized.into_prop());
113    self
114  }
115
116  /// Sets whether it asks to cover the whole screen.
117  pub fn fullscreen(mut self, fullscreen: impl IntoProp<bool>) -> FloatingWindow {
118    self.fullscreen = Some(fullscreen.into_prop());
119    self
120  }
121
122  /// Sets whether it is shown. Hidden, it has no surface at all; shown
123  /// again, it is a new window to the compositor.
124  pub fn visible(mut self, visible: impl IntoProp<bool>) -> FloatingWindow {
125    self.visible = Some(visible.into_prop());
126    self
127  }
128
129  /// Asks the compositor to blur what is behind this part of the window,
130  /// as [`PanelWindow::blur`](crate::PanelWindow::blur) does.
131  pub fn blur(mut self, region: impl IntoProp<Option<Region>>) -> FloatingWindow {
132    self.hints.blur = Some(region.into_prop());
133    self
134  }
135
136  /// Keeps the screen from idling while `true` and the window is shown,
137  /// as [`PanelWindow::inhibit_idle`](crate::PanelWindow::inhibit_idle)
138  /// does.
139  pub fn inhibit_idle(mut self, inhibit: impl IntoProp<bool>) -> FloatingWindow {
140    self.hints.inhibit_idle = Some(inhibit.into_prop());
141    self
142  }
143
144  /// Takes the compositor's shortcuts while `true` and the window has the
145  /// keyboard, as
146  /// [`PanelWindow::inhibit_shortcuts`](crate::PanelWindow::inhibit_shortcuts)
147  /// does.
148  pub fn inhibit_shortcuts(mut self, inhibit: impl IntoProp<bool>) -> FloatingWindow {
149    self.hints.inhibit_shortcuts = Some(inhibit.into_prop());
150    self
151  }
152
153  /// Has the compositor write whether it honors `inhibit_shortcuts`, as
154  /// [`PanelWindow::shortcuts_inhibited`](crate::PanelWindow::shortcuts_inhibited)
155  /// does.
156  pub fn shortcuts_inhibited(mut self, signal: Signal<bool>) -> FloatingWindow {
157    self.hints.shortcuts_inhibited = Some(signal);
158    self
159  }
160
161  /// Holds the focus while `signal` is `true`, with the other windows
162  /// given it, as [`PanelWindow::focus_grab`](crate::PanelWindow::focus_grab)
163  /// does.
164  pub fn focus_grab(mut self, signal: Signal<bool>) -> FloatingWindow {
165    self.hints.focus_grab = Some(signal);
166    self
167  }
168
169  /// Runs `handler` when the user or compositor asks the window to close,
170  /// as with a title bar's close button. Without one, the window hides
171  /// until `visible` turns false and true again.
172  pub fn on_close(mut self, handler: impl Fn(&Cx) + 'static) -> FloatingWindow {
173    self.on_close = Some(Rc::new(Handler::new(handler)));
174    self
175  }
176
177  /// Sets the window's style. Its color and font are inherited by every
178  /// [`Text`](crate::Text) in it that doesn't set its own.
179  pub fn style(mut self, style: impl IntoProp<Style>) -> FloatingWindow {
180    self.style = Some(style.into_prop());
181    self
182  }
183
184  /// Sets the window's one child, which fills the window.
185  pub fn child(mut self, child: impl Widget) -> FloatingWindow {
186    self.child = Some(Box::new(child));
187    self
188  }
189}
190
191/// What the platform needs to show a floating window.
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct FloatingConfig {
194  /// The title.
195  pub title: String,
196  /// The size asked for, in logical pixels.
197  pub width: u32,
198  /// The size asked for, in logical pixels.
199  pub height: u32,
200  /// The smallest size; 0 leaves a side free.
201  pub min_size: (u32, u32),
202  /// The largest size; 0 leaves a side free.
203  pub max_size: (u32, u32),
204  /// Whether it asks to be maximized.
205  pub maximized: bool,
206  /// Whether it asks to be fullscreen.
207  pub fullscreen: bool,
208  /// Whether it should have a surface: visible, and not closed since.
209  pub shown: bool,
210}
211
212/// What a widget asked of the floating window it is in. Moving and
213/// resizing need a pointer button held, so ask from a button's
214/// `on_press`; asked otherwise, they are ignored.
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216pub enum WindowRequest {
217  /// Let the user drag the window, as by its title bar.
218  Move,
219  /// Let the user resize the window from these edges.
220  Resize(Anchors),
221  /// Minimize it; the compositor may do nothing.
222  Minimize,
223}
224
225/// A floating window's settings in its tree.
226pub(crate) struct FloatingState {
227  title: String,
228  width: u32,
229  height: u32,
230  min_size: (u32, u32),
231  max_size: (u32, u32),
232  maximized: bool,
233  fullscreen: bool,
234  visible: bool,
235  /// Closed with no `on_close`: hidden until `visible` turns false.
236  dismissed: bool,
237  /// Kept in the reactive runtime, which drops the user's closure when the
238  /// window's owner goes.
239  on_close: Signal<Option<CloseHandler>>,
240  pub(crate) requests: Vec<WindowRequest>,
241}
242
243impl Window {
244  /// Builds `window` under a new child owner of `cx`, as
245  /// [`Window::new`] does for a panel.
246  pub fn floating(cx: &Cx, mut window: FloatingWindow) -> Window {
247    let child = untrack(|| window.child.take().map(|child| child.into_element()));
248    let path: Rc<str> = "FloatingWindow".into();
249    let cx = cx
250      .child()
251      .named(path.to_string())
252      .at(Rc::new(Anchor::default()));
253    let owner = cx.owner();
254    let on_close = cx.signal(window.on_close.take());
255    let tree = Rc::new_cyclic(|this| {
256      RefCell::new(Tree::new(
257        this.clone(),
258        "FloatingWindow",
259        Output::new(""),
260        cx,
261        path,
262      ))
263    });
264    borrow(&tree).mount_floating(window, child, on_close);
265    Window { tree, owner }
266  }
267
268  /// A floating window's settings; `None` for a panel or popup. Read
269  /// them only after the first flush, as [`surface`](Self::surface).
270  pub fn floating_config(&self) -> Option<FloatingConfig> {
271    let tree = self.tree.borrow();
272    let floating = tree.floating.as_ref()?;
273    let (width, height) = FloatingWindow::DEFAULT_SIZE;
274    Some(FloatingConfig {
275      title: floating.title.clone(),
276      width: if floating.width == 0 {
277        width
278      } else {
279        floating.width
280      },
281      height: if floating.height == 0 {
282        height
283      } else {
284        floating.height
285      },
286      min_size: floating.min_size,
287      max_size: floating.max_size,
288      maximized: floating.maximized,
289      fullscreen: floating.fullscreen,
290      shown: floating.visible && !floating.dismissed,
291    })
292  }
293
294  /// The user or compositor asked this floating window to close: its
295  /// `on_close` runs, or without one it hides.
296  pub fn request_close(&self) {
297    let call = {
298      let mut tree = borrow(&self.tree);
299      let root = tree.root;
300      let Some(floating) = tree.floating.as_mut() else {
301        return;
302      };
303      let handler = untrack(|| floating.on_close.try_with(Option::clone)).flatten();
304      let Some(handler) = handler else {
305        floating.dismissed = true;
306        tree.surface.changed = true;
307        return;
308      };
309      let Some(node) = tree.node(root) else {
310        return;
311      };
312      (node.cx.clone(), Rc::clone(&node.path), handler)
313    };
314    let (cx, path, handler) = call;
315    run_isolated(&path, "on_close", handler, |f| f(&cx));
316  }
317
318  /// Takes what the window's widgets asked of it since the last call.
319  pub fn take_requests(&self) -> Vec<WindowRequest> {
320    let mut tree = borrow(&self.tree);
321    tree
322      .floating
323      .as_mut()
324      .map(|floating| std::mem::take(&mut floating.requests))
325      .unwrap_or_default()
326  }
327}
328
329impl Cx {
330  /// Lets the user drag the floating window this widget is in, as by a
331  /// title bar. Call it from a button's `on_press`, while the pointer
332  /// button is held.
333  pub fn start_move(&self) {
334    self.request(WindowRequest::Move);
335  }
336
337  /// Lets the user resize the floating window this widget is in from
338  /// `edges`, such as `Edge::Bottom | Edge::Right`. Call it from a
339  /// button's `on_press`.
340  pub fn start_resize(&self, edges: impl Into<Anchors>) {
341    self.request(WindowRequest::Resize(edges.into()));
342  }
343
344  /// Asks to minimize the floating window this widget is in.
345  pub fn minimize(&self) {
346    self.request(WindowRequest::Minimize);
347  }
348
349  fn request(&self, request: WindowRequest) {
350    let Some((tree, _)) = self.anchor.as_ref().and_then(|anchor| anchor.get()) else {
351      return tracing::warn!("{request:?} needs a widget's context in a floating window");
352    };
353    let mut tree = borrow(&tree);
354    match tree.floating.as_mut() {
355      Some(floating) => floating.requests.push(request),
356      None => tracing::warn!("{request:?} works only in a floating window"),
357    }
358  }
359}
360
361impl Tree {
362  /// Applies a floating window's own properties and adds its child.
363  fn mount_floating(
364    &mut self,
365    window: FloatingWindow,
366    child: Option<crate::widgets::Element>,
367    on_close: Signal<Option<CloseHandler>>,
368  ) {
369    let (width, height) = FloatingWindow::DEFAULT_SIZE;
370    self.floating = Some(FloatingState {
371      title: String::new(),
372      width,
373      height,
374      min_size: (0, 0),
375      max_size: (0, 0),
376      maximized: false,
377      fullscreen: false,
378      visible: true,
379      dismissed: false,
380      on_close,
381      requests: Vec::new(),
382    });
383    let root = self.root;
384    let FloatingWindow {
385      title,
386      width,
387      height,
388      min_size,
389      max_size,
390      maximized,
391      fullscreen,
392      visible,
393      hints,
394      on_close: _,
395      style,
396      child: _,
397    } = window;
398    self.bind_opt(root, title, "title", |tree, _, value| {
399      tree.set_floating(|f| &mut f.title, value);
400    });
401    self.bind_opt(root, width, "width", |tree, _, value| {
402      tree.set_floating(|f| &mut f.width, value);
403    });
404    self.bind_opt(root, height, "height", |tree, _, value| {
405      tree.set_floating(|f| &mut f.height, value);
406    });
407    self.bind_opt(root, min_size, "min_size", |tree, _, value| {
408      tree.set_floating(|f| &mut f.min_size, value);
409    });
410    self.bind_opt(root, max_size, "max_size", |tree, _, value| {
411      tree.set_floating(|f| &mut f.max_size, value);
412    });
413    self.bind_opt(root, maximized, "maximized", |tree, _, value| {
414      tree.set_floating(|f| &mut f.maximized, value);
415    });
416    self.bind_opt(root, fullscreen, "fullscreen", |tree, _, value| {
417      tree.set_floating(|f| &mut f.fullscreen, value);
418    });
419    self.bind_opt(root, visible, "visible", |tree, _, value| {
420      // Hiding forgets a close, so showing again shows.
421      if !value {
422        tree.set_floating(|f| &mut f.dismissed, false);
423      }
424      tree.set_floating(|f| &mut f.visible, value);
425    });
426    self.bind_compositor_hints(hints);
427    self.mount_root(style, child);
428    self.surface.changed = true;
429  }
430
431  /// Sets one floating setting and remembers that the settings changed.
432  fn set_floating<T: PartialEq>(
433    &mut self,
434    field: impl FnOnce(&mut FloatingState) -> &mut T,
435    value: T,
436  ) {
437    let Some(floating) = self.floating.as_mut() else {
438      return;
439    };
440    let slot = field(floating);
441    if *slot != value {
442      *slot = value;
443      self.surface.changed = true;
444    }
445  }
446}