1use 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::{Anchors, Window};
18
19#[derive(Default)]
30pub struct PopupWindow {
31 style: Option<Prop<Style>>,
32 child: Option<Box<dyn Widget>>,
33 edges: Option<Prop<Anchors>>,
34 gravity: Option<Prop<Anchors>>,
35 adjustment: Option<Prop<Adjust>>,
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub struct Adjust(u8);
42
43impl Adjust {
44 pub const NONE: Adjust = Adjust(0);
46 pub const SLIDE_X: Adjust = Adjust(1);
48 pub const SLIDE_Y: Adjust = Adjust(2);
50 pub const FLIP_X: Adjust = Adjust(4);
52 pub const FLIP_Y: Adjust = Adjust(8);
54 pub const SLIDE: Adjust = Adjust(1 | 2);
56 pub const FLIP: Adjust = Adjust(4 | 8);
58 pub const ALL: Adjust = Adjust(15);
61
62 pub const fn contains(self, other: Adjust) -> bool {
64 self.0 & other.0 == other.0
65 }
66}
67
68impl std::ops::BitOr for Adjust {
69 type Output = Adjust;
70
71 fn bitor(self, other: Adjust) -> Adjust {
72 Adjust(self.0 | other.0)
73 }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub struct Placement {
82 pub edges: Option<Anchors>,
84 pub gravity: Option<Anchors>,
86 pub adjustment: Option<Adjust>,
88}
89
90impl PopupWindow {
91 pub fn new() -> PopupWindow {
93 PopupWindow::default()
94 }
95
96 pub fn style(mut self, style: impl IntoProp<Style>) -> PopupWindow {
99 self.style = Some(style.into_prop());
100 self
101 }
102
103 pub fn child(mut self, child: impl Widget) -> PopupWindow {
106 self.child = Some(Box::new(child));
107 self
108 }
109
110 pub fn edges(mut self, edges: impl IntoProp<Anchors>) -> PopupWindow {
114 self.edges = Some(edges.into_prop());
115 self
116 }
117
118 pub fn gravity(mut self, gravity: impl IntoProp<Anchors>) -> PopupWindow {
121 self.gravity = Some(gravity.into_prop());
122 self
123 }
124
125 pub fn adjustment(mut self, adjustment: impl IntoProp<Adjust>) -> PopupWindow {
128 self.adjustment = Some(adjustment.into_prop());
129 self
130 }
131}
132
133impl Cx {
134 pub fn open_popup(&self, build: impl FnOnce(&Cx) -> PopupWindow) {
144 let Some((anchor, (tree, node))) = self
145 .anchor
146 .clone()
147 .and_then(|anchor| anchor.get().map(|found| (anchor, found)))
148 else {
149 return tracing::warn!("open_popup needs a widget's context, such as a handler's");
150 };
151 let opener = {
152 let tree = tree
153 .try_borrow()
154 .expect("invariant: a window's tree is not borrowed while user code runs");
155 tree
156 .node(node)
157 .map(|node| node.path.clone())
158 .zip(Some(tree.output.clone()))
159 };
160 let Some((opener, output)) = opener else {
161 return;
162 };
163 let path: Rc<str> = format!("{opener}/PopupWindow").into();
164 let root = Rc::new(Anchor::default());
165 let cx = self.child().named(path.to_string()).at(Rc::clone(&root));
166 let built = panic::catch_unwind(AssertUnwindSafe(|| {
169 untrack(|| {
170 let popup = build(&cx);
171 let child = popup.child.map(|child| child.into_element());
172 (
173 popup.style,
174 child,
175 (popup.edges, popup.gravity, popup.adjustment),
176 )
177 })
178 }));
179 let (style, child, placement) = match built {
180 Ok(built) => built,
181 Err(payload) => {
182 tracing::error!(
183 "{path} panicked while it was built, so it doesn't open: {}",
184 panic_message(payload.as_ref())
185 );
186 cx.owner().dispose();
187 return;
188 }
189 };
190 let owner = cx.owner();
191 let alive = cx.signal(());
192 let popup = Rc::new_cyclic(|this| {
193 let mut tree = Tree::new(this.clone(), "PopupWindow", output, cx, path);
194 tree.popup = Some(PopupState {
195 opener: anchor,
196 close: false,
197 alive,
198 placement: Placement::default(),
199 });
200 RefCell::new(tree)
201 });
202 {
203 let mut tree = borrow(&popup);
204 tree.mount_root(style, child);
205 let (edges, gravity, adjustment) = placement;
206 let root = tree.root;
207 tree.bind_opt(root, edges, "edges", |tree, _, value| {
208 if let Some(popup) = tree.popup.as_mut() {
209 popup.placement.edges = Some(value);
210 }
211 });
212 tree.bind_opt(root, gravity, "gravity", |tree, _, value| {
213 if let Some(popup) = tree.popup.as_mut() {
214 popup.placement.gravity = Some(value);
215 }
216 });
217 tree.bind_opt(root, adjustment, "adjustment", |tree, _, value| {
218 if let Some(popup) = tree.popup.as_mut() {
219 popup.placement.adjustment = Some(value);
220 }
221 });
222 }
223 borrow(&tree).popups.push(Window { tree: popup, owner });
224 }
225
226 pub fn close_popup(&self) {
231 if !self.close_popups_if_any() {
232 tracing::warn!("close_popup was called outside a popup");
233 }
234 }
235
236 pub(crate) fn close_popups_if_any(&self) -> bool {
240 let mut next = self
241 .anchor
242 .as_ref()
243 .and_then(|anchor| anchor.get())
244 .map(|(tree, _)| tree);
245 let mut closed = false;
246 while let Some(tree) = next.take() {
247 let mut tree = borrow(&tree);
248 let Some(popup) = tree.popup.as_mut() else {
249 break;
250 };
251 popup.close = true;
252 closed = true;
253 next = popup.opener.get().map(|(tree, _)| tree);
254 }
255 closed
256 }
257}
258
259impl Window {
260 pub fn take_popups(&self) -> Vec<Window> {
262 std::mem::take(&mut borrow(&self.tree).popups)
263 }
264
265 pub fn anchor(&self) -> Option<Rect> {
270 let opener = Rc::clone(&self.tree.borrow().popup.as_ref()?.opener);
271 let (tree, node) = opener.get()?;
272 let tree = tree.borrow();
273 tree.node(node)?.rect
274 }
275
276 pub fn placement(&self) -> Placement {
279 let tree = self.tree.borrow();
280 tree
281 .popup
282 .as_ref()
283 .map(|popup| popup.placement)
284 .unwrap_or_default()
285 }
286
287 pub fn is_popup(&self) -> bool {
289 self.tree.borrow().popup.is_some()
290 }
291
292 pub fn wants_close(&self) -> bool {
295 let tree = self.tree.borrow();
296 tree
297 .popup
298 .as_ref()
299 .is_some_and(|popup| popup.close || popup.alive.try_with(|_| ()).is_none())
300 }
301}