tui_lipan/widgets/popover/
mod.rs1pub(crate) mod layout;
2pub(crate) mod node;
3pub(crate) mod reconcile;
4
5pub use node::PopoverNode;
6pub(crate) use reconcile::*;
7
8use crate::callback::Callback;
9use crate::core::element::{Element, ElementKind, IntoElement};
10use crate::overlay::OverlayScope;
11use crate::style::Length;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15pub enum PopoverPlacement {
16 #[default]
18 BelowStart,
19 BelowCenter,
21 BelowEnd,
23 AboveStart,
25 AboveCenter,
27 AboveEnd,
29 RightStart,
31 RightCenter,
33 RightEnd,
35 LeftStart,
37 LeftCenter,
39 LeftEnd,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub struct PopoverOffset {
46 pub x: i16,
48 pub y: i16,
50}
51
52impl PopoverOffset {
53 pub const ZERO: Self = Self { x: 0, y: 0 };
55}
56
57impl From<(i16, i16)> for PopoverOffset {
58 fn from(value: (i16, i16)) -> Self {
59 Self {
60 x: value.0,
61 y: value.1,
62 }
63 }
64}
65
66#[derive(Clone)]
68pub struct Popover {
69 pub(crate) trigger: Box<Element>,
70 pub(crate) content: Box<Element>,
71 pub(crate) on_close: Option<Callback<()>>,
72 pub(crate) open: bool,
73 pub(crate) scope: OverlayScope,
74 pub(crate) placement: PopoverPlacement,
75 pub(crate) offset: PopoverOffset,
76 pub(crate) clamp: bool,
77 pub(crate) auto_flip: bool,
78 pub(crate) min_trigger_width: bool,
79 pub(crate) fit_trigger_width: bool,
80 pub(crate) max_width: Option<Length>,
81 pub(crate) anchor: Option<(u16, u16)>,
82 pub(crate) capture_focus: bool,
83 pub(crate) auto_focus: bool,
84}
85
86impl Default for Popover {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Popover {
93 pub fn new() -> Self {
95 Self {
96 trigger: Box::new(crate::widgets::Spacer::new().into()),
97 content: Box::new(crate::widgets::Spacer::new().into()),
98 on_close: None,
99 open: false,
100 scope: OverlayScope::RootPortal,
101 placement: PopoverPlacement::default(),
102 offset: PopoverOffset::ZERO,
103 clamp: true,
104 auto_flip: true,
105 min_trigger_width: true,
106 fit_trigger_width: false,
107 max_width: None,
108 anchor: None,
109 capture_focus: true,
110 auto_focus: true,
111 }
112 }
113
114 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
116 self.trigger = Box::new(trigger.into());
117 self
118 }
119
120 pub fn content(mut self, content: impl IntoElement) -> Self {
122 self.content = Box::new(content.into());
123 self
124 }
125
126 pub fn open(mut self, open: bool) -> Self {
128 self.open = open;
129 self
130 }
131
132 pub fn scope(mut self, scope: OverlayScope) -> Self {
134 self.scope = scope;
135 self
136 }
137
138 pub fn capture_focus(mut self, capture_focus: bool) -> Self {
144 self.capture_focus = capture_focus;
145 self
146 }
147
148 pub fn auto_focus(mut self, auto_focus: bool) -> Self {
152 self.auto_focus = auto_focus;
153 self
154 }
155
156 pub fn placement(mut self, placement: PopoverPlacement) -> Self {
158 self.placement = placement;
159 self
160 }
161
162 pub fn offset(mut self, offset: impl Into<PopoverOffset>) -> Self {
164 self.offset = offset.into();
165 self
166 }
167
168 pub fn clamp(mut self, clamp: bool) -> Self {
170 self.clamp = clamp;
171 self
172 }
173
174 pub fn auto_flip(mut self, auto_flip: bool) -> Self {
176 self.auto_flip = auto_flip;
177 self
178 }
179
180 pub fn min_trigger_width(mut self, min_trigger_width: bool) -> Self {
186 self.min_trigger_width = min_trigger_width;
187 self
188 }
189
190 pub fn fit_trigger_width(mut self, fit_trigger_width: bool) -> Self {
192 self.fit_trigger_width = fit_trigger_width;
193 self
194 }
195
196 pub fn max_width(mut self, max_width: Length) -> Self {
202 self.max_width = Some(max_width);
203 self
204 }
205
206 pub fn anchor(mut self, anchor: Option<(u16, u16)>) -> Self {
208 self.anchor = anchor;
209 self
210 }
211
212 pub fn on_close(mut self, cb: Callback<()>) -> Self {
214 self.on_close = Some(cb);
215 self
216 }
217}
218
219impl From<Popover> for Element {
220 fn from(popover: Popover) -> Self {
221 Element::new(ElementKind::Popover(popover)).with_layout(crate::style::LayoutConstraints {
222 min_w: crate::style::Length::Px(0),
223 min_h: crate::style::Length::Px(0),
224 ..Default::default()
225 })
226 }
227}
228
229impl crate::layout::hash::LayoutHash for Popover {
230 fn layout_hash(
231 &self,
232 hasher: &mut impl std::hash::Hasher,
233 recurse: &dyn Fn(&Element) -> Option<u64>,
234 ) -> Option<()> {
235 use std::hash::Hash;
236 self.open.hash(hasher);
237 self.scope.hash(hasher);
238 self.placement.hash(hasher);
239 self.offset.hash(hasher);
240 self.min_trigger_width.hash(hasher);
241 self.fit_trigger_width.hash(hasher);
242 self.max_width.hash(hasher);
243 self.anchor.hash(hasher);
244 self.capture_focus.hash(hasher);
245 self.auto_focus.hash(hasher);
246 recurse(self.trigger.as_ref())?.hash(hasher);
247 Some(())
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::core::element::ElementKind;
255
256 #[test]
257 fn popover_defaults_to_root_portal_scope() {
258 let element: Element = Popover::new().into();
259
260 let ElementKind::Popover(popover) = element.kind else {
261 panic!("expected popover element");
262 };
263
264 assert_eq!(popover.scope, OverlayScope::RootPortal);
265 assert!(popover.capture_focus);
266 }
267
268 #[test]
269 fn popover_scope_builder_updates_scope() {
270 let element: Element = Popover::new().scope(OverlayScope::Local).into();
271
272 let ElementKind::Popover(popover) = element.kind else {
273 panic!("expected popover element");
274 };
275
276 assert_eq!(popover.scope, OverlayScope::Local);
277 }
278
279 #[test]
280 fn popover_capture_focus_builder_updates_capture() {
281 let element: Element = Popover::new().capture_focus(false).into();
282
283 let ElementKind::Popover(popover) = element.kind else {
284 panic!("expected popover element");
285 };
286
287 assert!(!popover.capture_focus);
288 }
289}