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}