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  /// Sets the part of the window that takes pointer input, as
130  /// [`PanelWindow::mask`](crate::PanelWindow::mask) does: clicks outside
131  /// it reach whatever is below, and `None`, the default, is the whole
132  /// window.
133  pub fn mask(mut self, mask: impl IntoProp<Option<Region>>) -> FloatingWindow {
134    self.hints.mask = Some(mask.into_prop());
135    self
136  }
137
138  /// Asks the compositor to blur what is behind this part of the window,
139  /// as [`PanelWindow::blur`](crate::PanelWindow::blur) does.
140  pub fn blur(mut self, region: impl IntoProp<Option<Region>>) -> FloatingWindow {
141    self.hints.blur = Some(region.into_prop());
142    self
143  }
144
145  /// Keeps the screen from idling while `true` and the window is shown,
146  /// as [`PanelWindow::inhibit_idle`](crate::PanelWindow::inhibit_idle)
147  /// does.
148  pub fn inhibit_idle(mut self, inhibit: impl IntoProp<bool>) -> FloatingWindow {
149    self.hints.inhibit_idle = Some(inhibit.into_prop());
150    self
151  }
152
153  /// Takes the compositor's shortcuts while `true` and the window has the
154  /// keyboard, as
155  /// [`PanelWindow::inhibit_shortcuts`](crate::PanelWindow::inhibit_shortcuts)
156  /// does.
157  pub fn inhibit_shortcuts(mut self, inhibit: impl IntoProp<bool>) -> FloatingWindow {
158    self.hints.inhibit_shortcuts = Some(inhibit.into_prop());
159    self
160  }
161
162  /// Has the compositor write whether it honors `inhibit_shortcuts`, as
163  /// [`PanelWindow::shortcuts_inhibited`](crate::PanelWindow::shortcuts_inhibited)
164  /// does.
165  pub fn shortcuts_inhibited(mut self, signal: Signal<bool>) -> FloatingWindow {
166    self.hints.shortcuts_inhibited = Some(signal);
167    self
168  }
169
170  /// Holds the focus while `signal` is `true`, with the other windows
171  /// given it, as [`PanelWindow::focus_grab`](crate::PanelWindow::focus_grab)
172  /// does.
173  pub fn focus_grab(mut self, signal: Signal<bool>) -> FloatingWindow {
174    self.hints.focus_grab = Some(signal);
175    self
176  }
177
178  /// Sets the opacity the compositor shows the whole window at, blur
179  /// included, as
180  /// [`PanelWindow::compositor_opacity`](crate::PanelWindow::compositor_opacity)
181  /// does.
182  pub fn compositor_opacity(mut self, opacity: impl IntoProp<f32>) -> FloatingWindow {
183    self.hints.compositor_opacity = Some(opacity.into_prop());
184    self
185  }
186
187  /// Tells the compositor which part of the window holds anything to
188  /// see, as [`PanelWindow::visible_mask`](crate::PanelWindow::visible_mask)
189  /// does.
190  pub fn visible_mask(mut self, region: impl IntoProp<Option<Region>>) -> FloatingWindow {
191    self.hints.visible_mask = Some(region.into_prop());
192    self
193  }
194
195  /// Runs `handler` when the user or compositor asks the window to close,
196  /// as with a title bar's close button. Without one, the window hides
197  /// until `visible` turns false and true again.
198  pub fn on_close(mut self, handler: impl Fn(&Cx) + 'static) -> FloatingWindow {
199    self.on_close = Some(Rc::new(Handler::new(handler)));
200    self
201  }
202
203  /// Sets the window's style. Its color and font are inherited by every
204  /// [`Text`](crate::Text) in it that doesn't set its own.
205  pub fn style(mut self, style: impl IntoProp<Style>) -> FloatingWindow {
206    self.style = Some(style.into_prop());
207    self
208  }
209
210  /// Sets the window's one child, which fills the window.
211  pub fn child(mut self, child: impl Widget) -> FloatingWindow {
212    self.child = Some(Box::new(child));
213    self
214  }
215}
216
217/// What the platform needs to show a floating window.
218#[derive(Clone, Debug, PartialEq, Eq)]
219pub struct FloatingConfig {
220  /// The title.
221  pub title: String,
222  /// The size asked for, in logical pixels.
223  pub width: u32,
224  /// The size asked for, in logical pixels.
225  pub height: u32,
226  /// The smallest size; 0 leaves a side free.
227  pub min_size: (u32, u32),
228  /// The largest size; 0 leaves a side free.
229  pub max_size: (u32, u32),
230  /// Whether it asks to be maximized.
231  pub maximized: bool,
232  /// Whether it asks to be fullscreen.
233  pub fullscreen: bool,
234  /// Whether it should have a surface: visible, and not closed since.
235  pub shown: bool,
236}
237
238/// What a widget asked of the floating window it is in. Moving and
239/// resizing need a pointer button held, so ask from a button's
240/// `on_press`; asked otherwise, they are ignored.
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
242pub enum WindowRequest {
243  /// Let the user drag the window, as by its title bar.
244  Move,
245  /// Let the user resize the window from these edges.
246  Resize(Anchors),
247  /// Minimize it; the compositor may do nothing.
248  Minimize,
249}
250
251/// A floating window's settings in its tree.
252pub(crate) struct FloatingState {
253  title: String,
254  width: u32,
255  height: u32,
256  min_size: (u32, u32),
257  max_size: (u32, u32),
258  maximized: bool,
259  fullscreen: bool,
260  visible: bool,
261  /// Closed with no `on_close`: hidden until `visible` turns false.
262  dismissed: bool,
263  /// Kept in the reactive runtime, which drops the user's closure when the
264  /// window's owner goes.
265  on_close: Signal<Option<CloseHandler>>,
266  pub(crate) requests: Vec<WindowRequest>,
267}
268
269impl Window {
270  /// Builds `window` under a new child owner of `cx`, as
271  /// [`Window::new`] does for a panel.
272  pub fn floating(cx: &Cx, mut window: FloatingWindow) -> Window {
273    let child = untrack(|| window.child.take().map(|child| child.into_element()));
274    let path: Rc<str> = "FloatingWindow".into();
275    let cx = cx
276      .child()
277      .named(path.to_string())
278      .at(Rc::new(Anchor::default()));
279    let owner = cx.owner();
280    let on_close = cx.signal(window.on_close.take());
281    let tree = Rc::new_cyclic(|this| {
282      RefCell::new(Tree::new(
283        this.clone(),
284        "FloatingWindow",
285        Output::new(""),
286        cx,
287        path,
288      ))
289    });
290    borrow(&tree).mount_floating(window, child, on_close);
291    Window { tree, owner }
292  }
293
294  /// A floating window's settings; `None` for a panel or popup. Read
295  /// them only after the first flush, as [`surface`](Self::surface).
296  pub fn floating_config(&self) -> Option<FloatingConfig> {
297    let tree = self.tree.borrow();
298    let floating = tree.floating.as_ref()?;
299    let (width, height) = FloatingWindow::DEFAULT_SIZE;
300    Some(FloatingConfig {
301      title: floating.title.clone(),
302      width: if floating.width == 0 {
303        width
304      } else {
305        floating.width
306      },
307      height: if floating.height == 0 {
308        height
309      } else {
310        floating.height
311      },
312      min_size: floating.min_size,
313      max_size: floating.max_size,
314      maximized: floating.maximized,
315      fullscreen: floating.fullscreen,
316      shown: floating.visible && !floating.dismissed,
317    })
318  }
319
320  /// The user or compositor asked this floating window to close: its
321  /// `on_close` runs, or without one it hides.
322  pub fn request_close(&self) {
323    let call = {
324      let mut tree = borrow(&self.tree);
325      let root = tree.root;
326      let Some(floating) = tree.floating.as_mut() else {
327        return;
328      };
329      let handler = untrack(|| floating.on_close.try_with(Option::clone)).flatten();
330      let Some(handler) = handler else {
331        floating.dismissed = true;
332        tree.surface.changed = true;
333        return;
334      };
335      let Some(node) = tree.node(root) else {
336        return;
337      };
338      (node.cx.clone(), Rc::clone(&node.path), handler)
339    };
340    let (cx, path, handler) = call;
341    run_isolated(&path, "on_close", handler, |f| f(&cx));
342  }
343
344  /// Takes what the window's widgets asked of it since the last call.
345  pub fn take_requests(&self) -> Vec<WindowRequest> {
346    let mut tree = borrow(&self.tree);
347    tree
348      .floating
349      .as_mut()
350      .map(|floating| std::mem::take(&mut floating.requests))
351      .unwrap_or_default()
352  }
353}
354
355impl Cx {
356  /// Lets the user drag the floating window this widget is in, as by a
357  /// title bar. Call it from a button's `on_press`, while the pointer
358  /// button is held.
359  pub fn start_move(&self) {
360    self.request(WindowRequest::Move);
361  }
362
363  /// Lets the user resize the floating window this widget is in from
364  /// `edges`, such as `Edge::Bottom | Edge::Right`. Call it from a
365  /// button's `on_press`.
366  pub fn start_resize(&self, edges: impl Into<Anchors>) {
367    self.request(WindowRequest::Resize(edges.into()));
368  }
369
370  /// Asks to minimize the floating window this widget is in.
371  pub fn minimize(&self) {
372    self.request(WindowRequest::Minimize);
373  }
374
375  fn request(&self, request: WindowRequest) {
376    let Some((tree, _)) = self.anchor.as_ref().and_then(|anchor| anchor.get()) else {
377      return tracing::warn!("{request:?} needs a widget's context in a floating window");
378    };
379    let mut tree = borrow(&tree);
380    match tree.floating.as_mut() {
381      Some(floating) => floating.requests.push(request),
382      None => tracing::warn!("{request:?} works only in a floating window"),
383    }
384  }
385}
386
387impl Tree {
388  /// Applies a floating window's own properties and adds its child.
389  fn mount_floating(
390    &mut self,
391    window: FloatingWindow,
392    child: Option<crate::widgets::Element>,
393    on_close: Signal<Option<CloseHandler>>,
394  ) {
395    let (width, height) = FloatingWindow::DEFAULT_SIZE;
396    self.floating = Some(FloatingState {
397      title: String::new(),
398      width,
399      height,
400      min_size: (0, 0),
401      max_size: (0, 0),
402      maximized: false,
403      fullscreen: false,
404      visible: true,
405      dismissed: false,
406      on_close,
407      requests: Vec::new(),
408    });
409    let root = self.root;
410    let FloatingWindow {
411      title,
412      width,
413      height,
414      min_size,
415      max_size,
416      maximized,
417      fullscreen,
418      visible,
419      hints,
420      on_close: _,
421      style,
422      child: _,
423    } = window;
424    self.bind_opt(root, title, "title", |tree, _, value| {
425      tree.set_floating(|f| &mut f.title, value);
426    });
427    self.bind_opt(root, width, "width", |tree, _, value| {
428      tree.set_floating(|f| &mut f.width, value);
429    });
430    self.bind_opt(root, height, "height", |tree, _, value| {
431      tree.set_floating(|f| &mut f.height, value);
432    });
433    self.bind_opt(root, min_size, "min_size", |tree, _, value| {
434      tree.set_floating(|f| &mut f.min_size, value);
435    });
436    self.bind_opt(root, max_size, "max_size", |tree, _, value| {
437      tree.set_floating(|f| &mut f.max_size, value);
438    });
439    self.bind_opt(root, maximized, "maximized", |tree, _, value| {
440      tree.set_floating(|f| &mut f.maximized, value);
441    });
442    self.bind_opt(root, fullscreen, "fullscreen", |tree, _, value| {
443      tree.set_floating(|f| &mut f.fullscreen, value);
444    });
445    self.bind_opt(root, visible, "visible", |tree, _, value| {
446      // Hiding forgets a close, so showing again shows.
447      if !value {
448        tree.set_floating(|f| &mut f.dismissed, false);
449      }
450      tree.set_floating(|f| &mut f.visible, value);
451    });
452    self.bind_compositor_hints(hints);
453    self.mount_root(style, child);
454    self.surface.changed = true;
455  }
456
457  /// Sets one floating setting and remembers that the settings changed.
458  fn set_floating<T: PartialEq>(
459    &mut self,
460    field: impl FnOnce(&mut FloatingState) -> &mut T,
461    value: T,
462  ) {
463    let Some(floating) = self.floating.as_mut() else {
464      return;
465    };
466    let slot = field(floating);
467    if *slot != value {
468      *slot = value;
469      self.surface.changed = true;
470    }
471  }
472}