Skip to main content

crabshell_ui/
popup.rs

1//! Popups: [`PopupWindow`], the description of one, and how a widget opens
2//! and closes it with [`Cx::open_popup`] and [`Cx::close_popup`].
3
4use std::cell::RefCell;
5use std::panic::{self, AssertUnwindSafe};
6use std::rc::Rc;
7
8use crabshell_reactive::untrack;
9
10use crate::cx::{Anchor, Cx};
11use crate::geometry::Rect;
12use crate::input::panic_message;
13use crate::prop::{IntoProp, Prop};
14use crate::style::Style;
15use crate::tree::{PopupState, Tree, borrow};
16use crate::widgets::Widget;
17use crate::window::Window;
18
19/// A popup: a window next to the widget that opened it, sized to fit its
20/// one child. Open one with [`Cx::open_popup`].
21///
22/// ```
23/// use crabshell_ui::{Button, PopupWindow, Text};
24///
25/// let button = Button::new(Text::new("open")).on_click(|cx| {
26///     cx.open_popup(|_cx| PopupWindow::new().child(Text::new("hello")))
27/// });
28/// ```
29#[derive(Default)]
30pub struct PopupWindow {
31  style: Option<Prop<Style>>,
32  child: Option<Box<dyn Widget>>,
33}
34
35impl PopupWindow {
36  /// An empty popup.
37  pub fn new() -> PopupWindow {
38    PopupWindow::default()
39  }
40
41  /// Sets the popup's style. Its color and font are inherited by every
42  /// [`Text`](crate::Text) in it that doesn't set its own.
43  pub fn style(mut self, style: impl IntoProp<Style>) -> PopupWindow {
44    self.style = Some(style.into_prop());
45    self
46  }
47
48  /// Sets the popup's one child. The popup is as large as the child wants
49  /// to be, plus the style's padding.
50  pub fn child(mut self, child: impl Widget) -> PopupWindow {
51    self.child = Some(Box::new(child));
52    self
53  }
54}
55
56impl Cx {
57  /// Opens a popup next to the widget this context belongs to, such as the
58  /// button whose handler got it. `build` describes the popup and gets its
59  /// context: what it creates there goes when the popup does.
60  ///
61  /// The popup belongs to the widget, so it closes when the widget goes,
62  /// as well as on [`close_popup`](Self::close_popup) and when the user
63  /// clicks outside it. If `build` panics, that is logged with the
64  /// widget's path and nothing opens. A context of no widget, such as the
65  /// one `per_output` gives, logs that and opens nothing.
66  pub fn open_popup(&self, build: impl FnOnce(&Cx) -> PopupWindow) {
67    let Some((anchor, (tree, node))) = self
68      .anchor
69      .clone()
70      .and_then(|anchor| anchor.get().map(|found| (anchor, found)))
71    else {
72      return tracing::warn!("open_popup needs a widget's context, such as a handler's");
73    };
74    let opener = {
75      let tree = tree
76        .try_borrow()
77        .expect("invariant: a window's tree is not borrowed while user code runs");
78      tree
79        .node(node)
80        .map(|node| node.path.clone())
81        .zip(Some(tree.output.clone()))
82    };
83    let Some((opener, output)) = opener else {
84      return;
85    };
86    let path: Rc<str> = format!("{opener}/PopupWindow").into();
87    let root = Rc::new(Anchor::default());
88    let cx = self.child().named(path.to_string()).at(Rc::clone(&root));
89    // Untracked, so an effect that opens a popup doesn't rerun, and open
90    // another, when something `build` read changes.
91    let built = panic::catch_unwind(AssertUnwindSafe(|| {
92      untrack(|| {
93        let popup = build(&cx);
94        let child = popup.child.map(|child| child.into_element());
95        (popup.style, child)
96      })
97    }));
98    let (style, child) = match built {
99      Ok(built) => built,
100      Err(payload) => {
101        tracing::error!(
102          "{path} panicked while it was built, so it doesn't open: {}",
103          panic_message(payload.as_ref())
104        );
105        cx.owner().dispose();
106        return;
107      }
108    };
109    let owner = cx.owner();
110    let alive = cx.signal(());
111    let popup = Rc::new_cyclic(|this| {
112      let mut tree = Tree::new(this.clone(), "PopupWindow", output, cx, path);
113      tree.popup = Some(PopupState {
114        opener: anchor,
115        close: false,
116        alive,
117      });
118      RefCell::new(tree)
119    });
120    borrow(&popup).mount_root(style, child);
121    borrow(&tree).popups.push(Window { tree: popup, owner });
122  }
123
124  /// Closes the popup this context's widget is in, with every popup it
125  /// opened and every popup that opened it, as a menu does once one of
126  /// its entries is clicked. Outside a popup it logs that and does
127  /// nothing.
128  pub fn close_popup(&self) {
129    if !self.close_popups_if_any() {
130      tracing::warn!("close_popup was called outside a popup");
131    }
132  }
133
134  /// [`close_popup`](Self::close_popup), saying whether there was a popup
135  /// to close. It marks this popup and each popup that opened it, one
136  /// tree borrowed at a time; the popups they opened close with them.
137  pub(crate) fn close_popups_if_any(&self) -> bool {
138    let mut next = self
139      .anchor
140      .as_ref()
141      .and_then(|anchor| anchor.get())
142      .map(|(tree, _)| tree);
143    let mut closed = false;
144    while let Some(tree) = next.take() {
145      let mut tree = borrow(&tree);
146      let Some(popup) = tree.popup.as_mut() else {
147        break;
148      };
149      popup.close = true;
150      closed = true;
151      next = popup.opener.get().map(|(tree, _)| tree);
152    }
153    closed
154  }
155}
156
157impl Window {
158  /// Takes the popups this window's widgets opened since the last call.
159  pub fn take_popups(&self) -> Vec<Window> {
160    std::mem::take(&mut borrow(&self.tree).popups)
161  }
162
163  /// For a popup, the rectangle of the widget that opened it, at its
164  /// window's last layout, in that window's logical pixels. The platform
165  /// places the popup next to it. `None` for a panel, once the widget is
166  /// gone, or before its first layout.
167  pub fn anchor(&self) -> Option<Rect> {
168    let opener = Rc::clone(&self.tree.borrow().popup.as_ref()?.opener);
169    let (tree, node) = opener.get()?;
170    let tree = tree.borrow();
171    tree.node(node)?.rect
172  }
173
174  /// Whether this window is a popup.
175  pub fn is_popup(&self) -> bool {
176    self.tree.borrow().popup.is_some()
177  }
178
179  /// Whether this popup should close: [`Cx::close_popup`] asked, or the
180  /// widget that opened it is gone. Always `false` for a panel.
181  pub fn wants_close(&self) -> bool {
182    let tree = self.tree.borrow();
183    tree
184      .popup
185      .as_ref()
186      .is_some_and(|popup| popup.close || popup.alive.try_with(|_| ()).is_none())
187  }
188}