1use iced::keyboard::key::Named;
14use iced::widget::container;
15use iced::{Element, Length};
16
17use crate::keys::{self, Chord, Keymap};
18use crate::overlay::anchored::{Placement, anchored, surface_style};
19use crate::theme::{Tokens, space};
20
21pub use crate::overlay::anchored::{Align, Side};
22
23pub const WIDTH: f32 = 288.0;
25
26pub struct Popover<'a, Message> {
28 trigger: Element<'a, Message>,
29 content: Element<'a, Message>,
30 open: bool,
31 on_dismiss: Option<Message>,
32 placement: Placement,
33 width: Length,
34 padding: f32,
35 keymap: Keymap<Action>,
36}
37
38impl<Message> std::fmt::Debug for Popover<'_, Message> {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("Popover")
41 .field("open", &self.open)
42 .field("dismissable", &self.on_dismiss.is_some())
43 .field("placement", &self.placement)
44 .field("width", &self.width)
45 .field("padding", &self.padding)
46 .field("keymap", &self.keymap)
47 .finish_non_exhaustive()
48 }
49}
50
51pub fn popover<'a, Message>(
55 trigger: impl Into<Element<'a, Message>>,
56 content: impl Into<Element<'a, Message>>,
57) -> Popover<'a, Message> {
58 Popover {
59 trigger: trigger.into(),
60 content: content.into(),
61 open: false,
62 on_dismiss: None,
63 placement: Placement::default(),
64 width: Length::Fixed(WIDTH),
65 padding: space::LG,
66 keymap: default_keymap(),
67 }
68}
69
70impl<'a, Message> Popover<'a, Message> {
71 pub fn open(mut self, open: bool) -> Self {
72 self.open = open;
73 self
74 }
75
76 pub fn on_dismiss(mut self, message: Message) -> Self {
79 self.on_dismiss = Some(message);
80 self
81 }
82
83 pub fn side(mut self, side: Side) -> Self {
85 self.placement.side = side;
86 self
87 }
88
89 pub fn align(mut self, align: Align) -> Self {
91 self.placement.align = align;
92 self
93 }
94
95 pub fn gap(mut self, gap: f32) -> Self {
97 self.placement.gap = gap;
98 self
99 }
100
101 pub fn width(mut self, width: impl Into<Length>) -> Self {
103 self.width = width.into();
104 self
105 }
106
107 pub fn padding(mut self, padding: f32) -> Self {
109 self.padding = padding;
110 self
111 }
112
113 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
116 self.keymap = keymap;
117 self
118 }
119}
120
121impl<'a, Message: Clone + 'a> From<Popover<'a, Message>> for Element<'a, Message> {
122 fn from(popover: Popover<'a, Message>) -> Self {
123 let Popover {
124 trigger,
125 content,
126 open,
127 on_dismiss,
128 placement,
129 width,
130 padding,
131 keymap,
132 } = popover;
133
134 let panel = open.then(|| {
135 container(content)
136 .width(width)
137 .padding(padding)
138 .style(|theme| style(&Tokens::of(theme)))
139 .into()
140 });
141 anchored(trigger)
142 .content(panel)
143 .placement(placement)
144 .pass_through(true)
145 .dismiss_keys(keymap.chords(&Action::Close).into_iter().cloned())
146 .on_dismiss_maybe(on_dismiss)
147 .into()
148 }
149}
150
151pub fn style(tokens: &Tokens) -> container::Style {
153 surface_style(tokens)
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
158pub enum Action {
159 Toggle,
161 Close,
162}
163
164impl Action {
165 pub fn apply(self, open: bool) -> Option<bool> {
168 match self {
169 Action::Toggle => Some(!open),
170 Action::Close => open.then_some(false),
171 }
172 }
173}
174
175impl keys::Action for Action {
176 const ALL: &'static [Self] = &[Action::Toggle, Action::Close];
177
178 fn defaults() -> Keymap<Self> {
179 default_keymap()
180 }
181
182 fn name(self) -> &'static str {
183 match self {
184 Action::Toggle => "Toggle",
185 Action::Close => "Close",
186 }
187 }
188
189 fn description(self) -> &'static str {
190 match self {
191 Action::Toggle => "Opens the popover, or closes it when it is open.",
192 Action::Close => "Closes the popover.",
193 }
194 }
195}
196
197pub fn default_keymap() -> Keymap<Action> {
210 Keymap::new().bind(Chord::named(Named::Escape), Action::Close)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::theme::{dark, light};
217 use iced::widget::text;
218
219 #[test]
220 fn builder_defaults_to_closed_below_and_centred() {
221 let popover: Popover<'_, ()> = popover(text("Open"), text("Content"));
222 assert!(!popover.open);
223 assert!(popover.on_dismiss.is_none());
224 assert_eq!(popover.placement, Placement::default());
225 assert_eq!(popover.placement.side, Side::Bottom);
226 assert_eq!(popover.placement.align, Align::Center);
227 assert_eq!(popover.width, Length::Fixed(WIDTH));
228 assert_eq!(popover.padding, space::LG);
229 assert_eq!(popover.keymap, default_keymap());
230 }
231
232 #[test]
233 fn builder_sets_every_option() {
234 let popover: Popover<'_, u8> = popover(text("Open"), text("Content"))
235 .open(true)
236 .on_dismiss(1)
237 .side(Side::Right)
238 .align(Align::End)
239 .gap(10.0)
240 .width(Length::Shrink)
241 .padding(4.0)
242 .keymap(Keymap::new());
243 assert!(popover.keymap.is_empty());
244 assert!(popover.open);
245 assert_eq!(popover.on_dismiss, Some(1));
246 assert_eq!(popover.placement.side, Side::Right);
247 assert_eq!(popover.placement.align, Align::End);
248 assert_eq!(popover.placement.gap, 10.0);
249 assert_eq!(popover.width, Length::Shrink);
250 assert_eq!(popover.padding, 4.0);
251 }
252
253 #[test]
254 fn panel_uses_the_shared_surface_in_both_themes() {
255 for theme in [light(), dark()] {
256 let tokens = Tokens::of(&theme);
257 assert_eq!(style(&tokens), surface_style(&tokens));
258 }
259 }
260
261 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
262 let chord: Chord = chord.parse().unwrap();
263 keymap.resolve(chord.key(), chord.modifiers())
264 }
265
266 #[test]
267 fn default_keymap_closes_on_escape_only() {
268 let keymap = default_keymap();
269 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
270 assert!(keymap.chords(&Action::Toggle).is_empty());
271 assert_eq!(keymap.len(), 1);
272 assert_eq!(Keymap::<Action>::defaults(), keymap);
273 }
274
275 #[test]
276 fn keymap_overrides_and_unbinding() {
277 let keymap = default_keymap()
278 .bind("I".parse().unwrap(), Action::Toggle)
279 .unbind(&"Escape".parse().unwrap());
280 assert_eq!(press(&keymap, "i"), Some(Action::Toggle));
281 assert_eq!(press(&keymap, "Escape"), None);
282 }
283
284 #[test]
285 fn actions_map_to_the_next_open_state() {
286 assert_eq!(Action::Toggle.apply(false), Some(true));
287 assert_eq!(Action::Toggle.apply(true), Some(false));
288 assert_eq!(Action::Close.apply(true), Some(false));
289 assert_eq!(Action::Close.apply(false), None);
290 }
291}