Skip to main content

crabshell_ui/
window.rs

1//! [`PanelWindow`], the description of a bar, and [`Window`], the live tree
2//! built from it that the platform lays out, draws and sends input to.
3
4use std::cell::RefCell;
5use std::ops::BitOr;
6use std::rc::Rc;
7
8use crabshell_reactive::{Owner, Signal, untrack};
9
10use crate::cx::{Anchor, Cx};
11use crate::geometry::{Rect, Size};
12use crate::input::Handler;
13use crate::keyboard::{KeyEvent, KeyHandler};
14use crate::prop::{IntoProp, Prop};
15use crate::region::Region;
16use crate::style::Style;
17use crate::text::TextSystem;
18use crate::tree::{Tree, borrow};
19use crate::view::WindowView;
20use crate::widgets::Widget;
21
22/// A monitor, as the compositor names it.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Output {
25  name: String,
26}
27
28impl Output {
29  /// An output called `name`, such as `"DP-1"`. The platform creates
30  /// these from the compositor's announcements.
31  pub fn new(name: impl Into<String>) -> Output {
32    Output { name: name.into() }
33  }
34
35  /// The output's name, such as `"DP-1"`. It is an owned `String` so a
36  /// `move` closure can keep it.
37  pub fn name(&self) -> String {
38    self.name.clone()
39  }
40}
41
42/// An edge of the output.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum Edge {
45  /// The top edge.
46  Top,
47  /// The bottom edge.
48  Bottom,
49  /// The left edge.
50  Left,
51  /// The right edge.
52  Right,
53}
54
55impl Edge {
56  const fn bit(self) -> u8 {
57    match self {
58      Edge::Top => 1,
59      Edge::Bottom => 2,
60      Edge::Left => 4,
61      Edge::Right => 8,
62    }
63  }
64}
65
66/// The edges a window is anchored to. Combine edges with `|`:
67/// `Edge::Top | Edge::Left | Edge::Right`.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub struct Anchors(u8);
70
71impl Anchors {
72  /// Anchored to no edge: the compositor centers the window.
73  pub const NONE: Anchors = Anchors(0);
74
75  /// Whether `edge` is one of the anchors.
76  pub const fn contains(self, edge: Edge) -> bool {
77    self.0 & edge.bit() != 0
78  }
79
80  /// The edge the window sits against and reserves space along: the only
81  /// anchor, or the one anchor whose two neighbors are also anchors (`Top`
82  /// for `Top | Left | Right`). `None` for a corner or no clear edge.
83  pub fn edge(self) -> Option<Edge> {
84    let [top, bottom, left, right] =
85      [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right].map(|edge| self.contains(edge));
86    match (top, bottom, left, right) {
87      (true, false, false, false) | (true, false, true, true) => Some(Edge::Top),
88      (false, true, false, false) | (false, true, true, true) => Some(Edge::Bottom),
89      (false, false, true, false) | (true, true, true, false) => Some(Edge::Left),
90      (false, false, false, true) | (true, true, false, true) => Some(Edge::Right),
91      _ => None,
92    }
93  }
94}
95
96impl From<Edge> for Anchors {
97  fn from(edge: Edge) -> Anchors {
98    Anchors(edge.bit())
99  }
100}
101
102impl BitOr for Edge {
103  type Output = Anchors;
104
105  fn bitor(self, other: Edge) -> Anchors {
106    Anchors(self.bit() | other.bit())
107  }
108}
109
110impl BitOr<Edge> for Anchors {
111  type Output = Anchors;
112
113  fn bitor(self, edge: Edge) -> Anchors {
114    Anchors(self.0 | edge.bit())
115  }
116}
117
118/// The layer-shell layer a window is drawn in, from bottom to top.
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
120pub enum Layer {
121  /// Below everything, with the wallpaper.
122  Background,
123  /// Below windows.
124  Bottom,
125  /// Above windows. The default.
126  #[default]
127  Top,
128  /// Above everything, fullscreen windows included.
129  Overlay,
130}
131
132/// Whether and when a window takes the keyboard.
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
134pub enum KeyboardFocus {
135  /// Never. The default.
136  #[default]
137  None,
138  /// Always, while the window is in the top or overlay layer: nothing
139  /// else gets keys. For launchers and other windows that hold the
140  /// keyboard until they close.
141  Exclusive,
142  /// When the user clicks it, like an ordinary window. A compositor older
143  /// than layer-shell version 4 can't do this, and the window then takes
144  /// no keyboard focus.
145  OnDemand,
146}
147
148/// Space between a window and the output edges it is anchored to, in
149/// logical pixels. Edges the window isn't anchored to ignore theirs.
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
151pub struct Margins {
152  /// From the top edge.
153  pub top: i32,
154  /// From the right edge.
155  pub right: i32,
156  /// From the bottom edge.
157  pub bottom: i32,
158  /// From the left edge.
159  pub left: i32,
160}
161
162impl Margins {
163  /// The same margin on every edge.
164  pub const fn all(margin: i32) -> Margins {
165    Margins::new(margin, margin, margin, margin)
166  }
167
168  /// Each edge's margin, clockwise from the top, as in CSS.
169  pub const fn new(top: i32, right: i32, bottom: i32, left: i32) -> Margins {
170    Margins {
171      top,
172      right,
173      bottom,
174      left,
175    }
176  }
177
178  /// The margin on `edge`.
179  pub const fn on(self, edge: Edge) -> i32 {
180    match edge {
181      Edge::Top => self.top,
182      Edge::Right => self.right,
183      Edge::Bottom => self.bottom,
184      Edge::Left => self.left,
185    }
186  }
187}
188
189/// A bar: a layer-shell surface on one output with one child.
190///
191/// This is a description; [`Window::new`] builds it. By default it takes
192/// no keyboard focus. Every property takes a value or a closure.
193///
194/// ```
195/// use crabshell_ui::{Edge, Output, PanelWindow, Text};
196///
197/// let output = Output::new("DP-1");
198/// let bar = PanelWindow::new(&output)
199///     .anchors(Edge::Top | Edge::Left | Edge::Right)
200///     .height(30)
201///     .child(Text::new("hello"));
202/// ```
203pub struct PanelWindow {
204  pub(crate) output: Output,
205  pub(crate) anchors: Option<Prop<Anchors>>,
206  pub(crate) width: Option<Prop<u32>>,
207  pub(crate) height: Option<Prop<u32>>,
208  pub(crate) exclusive_zone: Option<Prop<i32>>,
209  pub(crate) layer: Option<Prop<Layer>>,
210  pub(crate) margins: Option<Prop<Margins>>,
211  pub(crate) keyboard_focus: Option<Prop<KeyboardFocus>>,
212  pub(crate) namespace: Option<Prop<String>>,
213  pub(crate) mask: Option<Prop<Option<Region>>>,
214  pub(crate) hints: Hints,
215  pub(crate) on_key: Option<KeyHandler>,
216  pub(crate) style: Option<Prop<Style>>,
217  pub(crate) child: Option<Box<dyn Widget>>,
218}
219
220impl PanelWindow {
221  /// A window on `output`, anchored to no edge, in the top layer.
222  pub fn new(output: &Output) -> PanelWindow {
223    PanelWindow {
224      output: output.clone(),
225      anchors: None,
226      width: None,
227      height: None,
228      exclusive_zone: None,
229      layer: None,
230      margins: None,
231      keyboard_focus: None,
232      namespace: None,
233      mask: None,
234      hints: Hints::default(),
235      on_key: None,
236      style: None,
237      child: None,
238    }
239  }
240
241  /// Sets the edges the window is anchored to.
242  pub fn anchors(mut self, anchors: impl IntoProp<Anchors>) -> PanelWindow {
243    self.anchors = Some(anchors.into_prop());
244    self
245  }
246
247  /// Sets the width in logical pixels. 0, the default, lets the compositor
248  /// choose, which it does when the window is anchored left and right.
249  pub fn width(mut self, width: impl IntoProp<u32>) -> PanelWindow {
250    self.width = Some(width.into_prop());
251    self
252  }
253
254  /// Sets the height in logical pixels. 0, the default, lets the
255  /// compositor choose, which it does when the window is anchored top and
256  /// bottom.
257  pub fn height(mut self, height: impl IntoProp<u32>) -> PanelWindow {
258    self.height = Some(height.into_prop());
259    self
260  }
261
262  /// Sets how much space, in logical pixels, other windows keep clear
263  /// along the anchored edge. The default is the window's height for a top
264  /// or bottom bar and its width for a left or right one. wlroots
265  /// compositors add the margin on that edge to the zone. 0 keeps clear
266  /// of other windows' zones without reserving any; -1 ignores them and
267  /// covers them.
268  pub fn exclusive_zone(mut self, zone: impl IntoProp<i32>) -> PanelWindow {
269    self.exclusive_zone = Some(zone.into_prop());
270    self
271  }
272
273  /// Sets the layer.
274  pub fn layer(mut self, layer: impl IntoProp<Layer>) -> PanelWindow {
275    self.layer = Some(layer.into_prop());
276    self
277  }
278
279  /// Sets the space between the window and the edges it is anchored to.
280  pub fn margins(mut self, margins: impl IntoProp<Margins>) -> PanelWindow {
281    self.margins = Some(margins.into_prop());
282    self
283  }
284
285  /// Sets whether the window takes the keyboard. The default is never.
286  pub fn keyboard_focus(mut self, focus: impl IntoProp<KeyboardFocus>) -> PanelWindow {
287    self.keyboard_focus = Some(focus.into_prop());
288    self
289  }
290
291  /// Sets the layer-shell namespace, which compositors use to tell
292  /// windows apart, as in Sway's `layer_effects`. The default is
293  /// `"crabshell"`, which also replaces a name holding a NUL byte.
294  /// Compositors read it once, so a change after the window is shown
295  /// applies only when it is shown again, as on a new output.
296  pub fn namespace(mut self, namespace: impl IntoProp<String>) -> PanelWindow {
297    self.namespace = Some(namespace.into_prop());
298    self
299  }
300
301  /// Sets the part of the window that takes pointer input; clicks outside
302  /// it reach whatever is below. `None`, the default, is the whole window,
303  /// and [`Region::empty`] lets every click through.
304  pub fn mask(mut self, mask: impl IntoProp<Option<Region>>) -> PanelWindow {
305    self.mask = Some(mask.into_prop());
306    self
307  }
308
309  /// Asks the compositor to blur what is behind this part of the window,
310  /// so a translucent background looks frosted. `None`, the default,
311  /// blurs nothing. It needs `ext-background-effect-v1`; a compositor
312  /// without it shows the window unblurred, and the shell logs that once.
313  ///
314  /// ```
315  /// use crabshell_ui::{Output, PanelWindow, Region};
316  ///
317  /// // The whole of a 30 pixel bar, however wide the output.
318  /// let bar = PanelWindow::new(&Output::new("DP-1"))
319  ///     .height(30)
320  ///     .blur(Some(Region::rect(0, 0, u32::MAX, 30)));
321  /// ```
322  pub fn blur(mut self, region: impl IntoProp<Option<Region>>) -> PanelWindow {
323    self.hints.blur = Some(region.into_prop());
324    self
325  }
326
327  /// Keeps the screen from blanking or locking for idleness while `true`
328  /// and the window is shown, as a video player does. It needs
329  /// `idle-inhibit-unstable-v1`; without it, that is logged once.
330  pub fn inhibit_idle(mut self, inhibit: impl IntoProp<bool>) -> PanelWindow {
331    self.hints.inhibit_idle = Some(inhibit.into_prop());
332    self
333  }
334
335  /// While `true` and the window has the keyboard, the compositor's own
336  /// shortcuts, such as Sway's `bindsym`s, go to the window as keys
337  /// instead, as a remote desktop or a game wants. The compositor may
338  /// keep some, such as one that takes them back. It needs
339  /// `keyboard-shortcuts-inhibit-unstable-v1`; without it, that is logged
340  /// once.
341  pub fn inhibit_shortcuts(mut self, inhibit: impl IntoProp<bool>) -> PanelWindow {
342    self.hints.inhibit_shortcuts = Some(inhibit.into_prop());
343    self
344  }
345
346  /// The compositor writes `true` to `signal` while it honors
347  /// [`inhibit_shortcuts`](Self::inhibit_shortcuts), and `false` once it
348  /// stops, as when the user takes its shortcuts back, or the window
349  /// stops asking or goes. Give each window a signal of its own.
350  pub fn shortcuts_inhibited(mut self, signal: Signal<bool>) -> PanelWindow {
351    self.hints.shortcuts_inhibited = Some(signal);
352    self
353  }
354
355  /// While `signal` is `true`, this window and the shown windows given
356  /// the same signal, with the popups they open, hold the keyboard and
357  /// pointer focus: a click anywhere else ends the grab, and the
358  /// compositor writes `false` to `signal`, as it does when every one of
359  /// them hides. Writing `false` ends the grab, and `true` asks again. It
360  /// needs Hyprland's `hyprland-focus-grab-v1`; without it, that is
361  /// logged once and `signal` is left as it is.
362  pub fn focus_grab(mut self, signal: Signal<bool>) -> PanelWindow {
363    self.hints.focus_grab = Some(signal);
364    self
365  }
366
367  /// Runs `handler` for each key the window gets that its focused button
368  /// didn't use. It returns `true` when it used the key; otherwise Tab,
369  /// Enter and Space do what they do by default. The window gets keys
370  /// only with a [`keyboard_focus`](Self::keyboard_focus).
371  pub fn on_key(mut self, handler: impl Fn(&Cx, &KeyEvent) -> bool + 'static) -> PanelWindow {
372    self.on_key = Some(Rc::new(Handler::new(handler)));
373    self
374  }
375
376  /// Sets the window's style. Its color and font are inherited by every
377  /// [`Text`](crate::Text) in the window that doesn't set its own.
378  pub fn style(mut self, style: impl IntoProp<Style>) -> PanelWindow {
379    self.style = Some(style.into_prop());
380    self
381  }
382
383  /// Sets the window's one child, which fills the window.
384  pub fn child(mut self, child: impl Widget) -> PanelWindow {
385    self.child = Some(Box::new(child));
386    self
387  }
388}
389
390/// What a panel or floating window asks of the compositor besides its
391/// surface's settings.
392#[derive(Default)]
393pub(crate) struct Hints {
394  pub(crate) blur: Option<Prop<Option<Region>>>,
395  pub(crate) inhibit_idle: Option<Prop<bool>>,
396  pub(crate) inhibit_shortcuts: Option<Prop<bool>>,
397  pub(crate) shortcuts_inhibited: Option<Signal<bool>>,
398  pub(crate) focus_grab: Option<Signal<bool>>,
399}
400
401/// What the platform needs to set up the window's layer surface.
402#[derive(Clone, Debug, PartialEq)]
403pub struct SurfaceConfig {
404  /// The anchored edges.
405  pub anchors: Anchors,
406  /// The requested width; 0 lets the compositor choose.
407  pub width: u32,
408  /// The requested height; 0 lets the compositor choose.
409  pub height: u32,
410  /// The exclusive zone, with the default worked out.
411  pub exclusive_zone: i32,
412  /// The layer.
413  pub layer: Layer,
414  /// The margins from the anchored edges.
415  pub margins: Margins,
416  /// Whether the window takes the keyboard.
417  pub keyboard_focus: KeyboardFocus,
418  /// The layer-shell namespace.
419  pub namespace: String,
420  /// The part that takes pointer input; `None` is all of it.
421  pub mask: Option<Region>,
422}
423
424impl Default for SurfaceConfig {
425  /// A layer-shell surface's defaults: no anchors, sizes or zone, in the
426  /// top layer, without the keyboard.
427  fn default() -> SurfaceConfig {
428    SurfaceConfig {
429      anchors: Anchors::NONE,
430      width: 0,
431      height: 0,
432      exclusive_zone: 0,
433      layer: Layer::Top,
434      margins: Margins::default(),
435      keyboard_focus: KeyboardFocus::None,
436      namespace: String::from(DEFAULT_NAMESPACE),
437      mask: None,
438    }
439  }
440}
441
442/// The layer-shell namespace unless a window sets its own.
443pub(crate) const DEFAULT_NAMESPACE: &str = "crabshell";
444
445/// A live window: the tree of nodes built from a [`PanelWindow`].
446///
447/// The platform owns one per layer surface. Each event-loop iteration it
448/// runs the reactive flush, then calls [`layout`](Self::layout) if
449/// [`needs_layout`](Self::needs_layout), then draws
450/// [`take_damage`](Self::take_damage) from [`view`](Self::view). When
451/// nothing changed, there is nothing to lay out or draw.
452///
453/// Dropping the window disposes its owner, which frees every node,
454/// binding and handler in it.
455pub struct Window {
456  pub(crate) tree: Rc<RefCell<Tree>>,
457  pub(crate) owner: Owner,
458}
459
460impl Window {
461  /// Builds `panel` under a new child owner of `cx`.
462  ///
463  /// Properties set to values take effect now. Bindings are effects, so
464  /// they first run at the next [`Runtime::flush`]; run it before the
465  /// first layout.
466  ///
467  /// [`Runtime::flush`]: crabshell_reactive::Runtime::flush
468  pub fn new(cx: &Cx, mut panel: PanelWindow) -> Window {
469    // Describe the whole tree first: that runs the user's `into_element`
470    // code, which must not see the tree borrowed or subscribe whatever
471    // effect is building this window.
472    let child = untrack(|| panel.child.take().map(|child| child.into_element()));
473    let path: Rc<str> = format!("PanelWindow[{}]", panel.output.name).into();
474    let cx = cx
475      .child()
476      .named(path.to_string())
477      .at(Rc::new(Anchor::default()));
478    let owner = cx.owner();
479    let output = panel.output.clone();
480    let tree =
481      Rc::new_cyclic(|this| RefCell::new(Tree::new(this.clone(), "PanelWindow", output, cx, path)));
482    borrow(&tree).mount_panel(panel, child);
483    Window { tree, owner }
484  }
485
486  /// The output the window is on.
487  pub fn output(&self) -> Output {
488    self.tree.borrow().output.clone()
489  }
490
491  /// The layer-surface settings. Read them only after the first
492  /// [`Runtime::flush`]: settings given as closures are bindings, which
493  /// first run then, and until they do these are the defaults.
494  ///
495  /// [`Runtime::flush`]: crabshell_reactive::Runtime::flush
496  pub fn surface(&self) -> SurfaceConfig {
497    let tree = self.tree.borrow();
498    let surface = &tree.surface;
499    let exclusive_zone = surface.exclusive_zone.unwrap_or_else(|| {
500      let thickness = match surface.anchors.edge() {
501        Some(Edge::Top | Edge::Bottom) => surface.height,
502        Some(Edge::Left | Edge::Right) => surface.width,
503        None => 0,
504      };
505      i32::try_from(thickness).unwrap_or(i32::MAX)
506    });
507    SurfaceConfig {
508      anchors: surface.anchors,
509      width: surface.width,
510      height: surface.height,
511      exclusive_zone,
512      layer: surface.layer,
513      margins: surface.margins,
514      keyboard_focus: surface.keyboard_focus,
515      namespace: surface.namespace.clone(),
516      mask: surface.mask.clone(),
517    }
518  }
519
520  /// The region a panel or floating window asks the compositor to blur
521  /// behind, as its `blur` property says.
522  pub fn blur(&self) -> Option<Region> {
523    self.tree.borrow().surface.blur.clone()
524  }
525
526  /// Whether a panel or floating window asks to keep the screen from
527  /// idling, as its `inhibit_idle` property says.
528  pub fn inhibits_idle(&self) -> bool {
529    self.tree.borrow().surface.inhibit_idle
530  }
531
532  /// Whether a panel or floating window asks for the compositor's
533  /// shortcuts while it has the keyboard, as its `inhibit_shortcuts`
534  /// property says.
535  pub fn inhibits_shortcuts(&self) -> bool {
536    self.tree.borrow().surface.inhibit_shortcuts
537  }
538
539  /// The signal a panel or floating window was given with
540  /// `shortcuts_inhibited`, which the platform writes.
541  pub fn shortcuts_inhibited(&self) -> Option<Signal<bool>> {
542    self.tree.borrow().surface.shortcuts_inhibited
543  }
544
545  /// The signal a panel or floating window was given with `focus_grab`:
546  /// windows given the same one share a grab.
547  pub fn focus_grab(&self) -> Option<Signal<bool>> {
548    self.tree.borrow().surface.focus_grab
549  }
550
551  /// Whether the settings in [`surface`](Self::surface) changed since the
552  /// last call. The first call returns `true`, even before the first flush,
553  /// when [`surface`](Self::surface) still holds the defaults.
554  pub fn take_surface_changed(&self) -> bool {
555    std::mem::take(&mut borrow(&self.tree).surface.changed)
556  }
557
558  /// The window's size in logical pixels: the size the compositor gave a
559  /// panel, or the size a popup's content wants, from its last layout.
560  pub fn size(&self) -> Size {
561    self.tree.borrow().size
562  }
563
564  /// Sets the size the compositor gave the window, from a configure
565  /// event. A new size needs a layout.
566  pub fn set_size(&self, size: Size) {
567    let mut tree = borrow(&self.tree);
568    if tree.size != size {
569      tree.size = size;
570      tree.needs_layout = true;
571    }
572  }
573
574  /// Whether something changed that needs [`layout`](Self::layout).
575  pub fn needs_layout(&self) -> bool {
576    self.tree.borrow().needs_layout
577  }
578
579  /// Lays the window out at its size, measuring text with `text`, and
580  /// records the areas that moved or changed as damage.
581  pub fn layout(&self, text: &mut TextSystem) {
582    borrow(&self.tree).layout(text);
583  }
584
585  /// Whether any area needs repainting.
586  pub fn has_damage(&self) -> bool {
587    !self.tree.borrow().damage.is_empty()
588  }
589
590  /// Takes the areas to repaint since the last call, in logical pixels.
591  /// They may overlap.
592  pub fn take_damage(&self) -> Vec<Rect> {
593    std::mem::take(&mut borrow(&self.tree).damage)
594  }
595
596  /// A read-only view of the laid-out tree, for drawing. Release it before
597  /// the next flush or pointer event.
598  pub fn view(&self) -> WindowView<'_> {
599    WindowView::new(self.tree.borrow())
600  }
601}
602
603impl Drop for Window {
604  fn drop(&mut self) {
605    // A `hovered` signal owned elsewhere mustn't stay `true`.
606    self.pointer_leave();
607    self.owner.dispose();
608  }
609}