Skip to main content

crabshell_ui/
lock.rs

1//! [`SessionLock`], a lock screen over every output, and [`Lock`], the
2//! live one the platform locks the session for and builds each output's
3//! lock surface from.
4
5use std::cell::RefCell;
6use std::panic::{self, AssertUnwindSafe};
7use std::rc::Rc;
8
9use crabshell_reactive::{ReadSignal, Signal, untrack};
10
11use crate::cx::{Anchor, Cx};
12use crate::input::panic_message;
13use crate::prop::{IntoProp, Prop, PropKind};
14use crate::style::Style;
15use crate::tree::{Tree, borrow};
16use crate::widgets::Widget;
17use crate::window::{Output, Window};
18
19/// A `surface` builder, as stored.
20type Build = Rc<dyn Fn(&Cx, &Output) -> Box<dyn Widget>>;
21
22/// The style, shared by every surface, and the builder.
23type Parts = (Option<Rc<dyn Fn() -> Style>>, Option<Build>);
24
25/// A lock screen: while `locked` is true, the compositor shows only its
26/// surfaces, one covering each output, and nothing else gets input. Lock
27/// with `cx.session_lock` from the `crabshell` crate. Every property
28/// takes a value or a closure.
29///
30/// ```
31/// use crabshell_ui::{Color, SessionLock, Style, Text};
32///
33/// let lock = SessionLock::new()
34///     .locked(true)
35///     .style(Style::new().background(Color::rgb(0, 0, 0)))
36///     .surface(|_cx, output| Text::new(format!("locked on {}", output.name())));
37/// ```
38#[derive(Default)]
39pub struct SessionLock {
40  locked: Option<Prop<bool>>,
41  style: Option<Prop<Style>>,
42  surface: Option<Build>,
43}
44
45impl SessionLock {
46  /// A lock that never locks, with nothing on its surfaces.
47  pub fn new() -> SessionLock {
48    SessionLock::default()
49  }
50
51  /// Sets whether the session is locked. Turning it false unlocks, as
52  /// after the user's password was checked.
53  pub fn locked(mut self, locked: impl IntoProp<bool>) -> SessionLock {
54    self.locked = Some(locked.into_prop());
55    self
56  }
57
58  /// Sets the style of every output's surface. Its color and font are
59  /// inherited by every [`Text`](crate::Text) on them that doesn't set its
60  /// own.
61  pub fn style(mut self, style: impl IntoProp<Style>) -> SessionLock {
62    self.style = Some(style.into_prop());
63    self
64  }
65
66  /// Sets what each output's surface shows: `build` runs for every output
67  /// while the session locks, and for each output added while it is
68  /// locked, under a context that goes with that surface.
69  pub fn surface<W: Widget>(mut self, build: impl Fn(&Cx, &Output) -> W + 'static) -> SessionLock {
70    self.surface = Some(Rc::new(move |cx: &Cx, output: &Output| {
71      Box::new(build(cx, output)) as Box<dyn Widget>
72    }));
73    self
74  }
75}
76
77/// A [`SessionLock`] as the platform holds it: whether the config wants
78/// the session locked, what the compositor says, and each output's lock
79/// surface. Dropping it disposes its owner, which frees everything it
80/// made.
81pub struct Lock {
82  cx: Cx,
83  /// What `locked` says.
84  wanted: Signal<bool>,
85  /// What the compositor says.
86  locked: Signal<bool>,
87  /// Kept in the reactive runtime, which drops the user's closures when
88  /// the lock's owner goes.
89  parts: Signal<Parts>,
90}
91
92impl Lock {
93  /// Builds `lock` under a new child owner of `cx`. A `locked` binding is
94  /// an effect, so it first runs at the next flush.
95  ///
96  /// # Panics
97  ///
98  /// If `cx`'s owner was disposed, as [`Cx::signal`] does.
99  pub fn new(cx: &Cx, lock: SessionLock) -> Lock {
100    let cx = cx.child().named("SessionLock");
101    let wanted = cx.signal(false);
102    match lock.locked.map(|locked| locked.0) {
103      None => {}
104      Some(PropKind::Value(value)) => wanted.set(value),
105      Some(PropKind::Binding(mut compute)) => {
106        cx.effect(move || wanted.set(compute()))
107          .named("SessionLock.locked");
108      }
109    }
110    Lock {
111      wanted,
112      locked: cx.signal(false),
113      parts: cx.signal((lock.style.map(Prop::shared), lock.surface)),
114      cx,
115    }
116  }
117
118  /// Whether `locked` says to lock; `None` once the lock's owner is gone.
119  pub fn wanted(&self) -> Option<bool> {
120    untrack(|| self.wanted.try_get())
121  }
122
123  /// `true` while the compositor says the session is locked.
124  pub fn locked(&self) -> ReadSignal<bool> {
125    self.locked.read_only()
126  }
127
128  /// Records what the compositor said; a value it holds already is left
129  /// alone, so nothing reruns.
130  pub fn set_locked(&self, locked: bool) {
131    if untrack(|| self.locked.try_get()) == Some(!locked) {
132      self.locked.set(locked);
133    }
134  }
135
136  /// Builds `output`'s lock surface under a new child owner, its path
137  /// `SessionLock[DP-1]`. A `surface` builder that panics is logged with
138  /// that path, and the surface shows only the lock's style. `None` once
139  /// the lock's owner is gone.
140  pub fn surface(&self, output: &Output) -> Option<Window> {
141    let (style, build) = untrack(|| self.parts.try_with(Clone::clone))?;
142    let path: Rc<str> = format!("SessionLock[{}]", output.name()).into();
143    let cx = self
144      .cx
145      .child()
146      .named(path.to_string())
147      .at(Rc::new(Anchor::default()));
148    // Untracked, as a popup's builder: nothing reruns it.
149    let built = panic::catch_unwind(AssertUnwindSafe(|| {
150      untrack(|| build.map(|build| build(&cx, output).into_element()))
151    }));
152    let child = built.unwrap_or_else(|payload| {
153      tracing::error!(
154        "{path} panicked while it was built, so it shows only the lock's style: {}",
155        panic_message(payload.as_ref())
156      );
157      None
158    });
159    let owner = cx.owner();
160    let tree = Rc::new_cyclic(|this| {
161      RefCell::new(Tree::new(
162        this.clone(),
163        "SessionLock",
164        output.clone(),
165        cx,
166        path,
167      ))
168    });
169    let style = style.map(|style| (move || style()).into_prop());
170    borrow(&tree).mount_root(style, child);
171    Some(Window { tree, owner })
172  }
173}
174
175impl Drop for Lock {
176  fn drop(&mut self) {
177    self.cx.owner().dispose();
178  }
179}