Skip to main content

freya_components/
popup.rs

1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::{
4    gaps::Gaps,
5    prelude::{
6        Alignment,
7        Position,
8    },
9    size::Size,
10};
11
12use crate::{
13    define_theme,
14    get_theme,
15};
16
17define_theme! {
18    %[component]
19    pub Popup {
20        %[fields]
21        background: Color,
22        color: Color,
23        width: Size,
24        height: Size,
25        padding: Gaps,
26        spacing: f32,
27    }
28}
29
30/// Popup background wrapper.
31#[derive(Clone, PartialEq)]
32pub struct PopupBackground {
33    pub children: Element,
34    pub on_press: EventHandler<Event<PressEventData>>,
35    pub background: Color,
36}
37
38impl PopupBackground {
39    pub fn new(
40        children: Element,
41        on_press: impl Into<EventHandler<Event<PressEventData>>>,
42        background: Color,
43    ) -> Self {
44        Self {
45            children,
46            on_press: on_press.into(),
47            background,
48        }
49    }
50}
51
52impl Component for PopupBackground {
53    fn render(&self) -> impl IntoElement {
54        let on_press = self.on_press.clone();
55
56        rect()
57            .child(
58                rect()
59                    .on_press(on_press)
60                    .position(Position::new_global().top(0.).left(0.))
61                    .height(Size::window_percent(100.))
62                    .width(Size::window_percent(100.))
63                    .background(self.background),
64            )
65            .child(
66                rect()
67                    .position(Position::new_global().top(0.).left(0.))
68                    .height(Size::window_percent(100.))
69                    .width(Size::window_percent(100.))
70                    .center()
71                    .child(self.children.clone()),
72            )
73    }
74}
75
76/// Floating popup / dialog.
77///
78/// The popup is shown whenever it has children, and the close animation plays
79/// when the children are removed. Conditionally attach children with
80/// [`MaybeExt::maybe`] or [`MaybeExt::map`].
81///
82/// # Example
83///
84/// ```rust
85/// # use freya::prelude::*;
86/// fn app() -> impl IntoElement {
87///     let mut show_popup = use_state(|| true);
88///
89///     rect()
90///         .child(
91///             Popup::new()
92///                 .width(Size::px(250.))
93///                 .on_close_request(move |_| show_popup.set(false))
94///                 .maybe(show_popup(), |popup| {
95///                     popup
96///                         .child(PopupTitle::new("Title".to_string()))
97///                         .child(PopupContent::new().child("Hello, World!"))
98///                         .child(
99///                             PopupButtons::new().child(
100///                                 Button::new()
101///                                     .on_press(move |_| show_popup.set(false))
102///                                     .expanded()
103///                                     .filled()
104///                                     .child("Accept"),
105///                             ),
106///                         )
107///                 }),
108///         )
109///         .child(
110///             Button::new()
111///                 .child("Open")
112///                 .on_press(move |_| show_popup.toggle()),
113///         )
114/// }
115/// # use freya_testing::prelude::*;
116/// # launch_doc(|| {
117/// #   rect().center().expanded().child(
118/// #      app()
119/// #   )
120/// # }, "./images/gallery_popup.png").with_scale_factor(0.8).with_hook(|test| {
121/// #   test.poll(std::time::Duration::from_millis(10), std::time::Duration::from_millis(500));
122/// # }).render();
123/// ```
124///
125/// # Preview
126/// ![Popup Preview][popup]
127#[doc(alias = "alert")]
128#[doc(alias = "dialog")]
129#[doc(alias = "window")]
130#[cfg_attr(feature = "docs",
131    doc = embed_doc_image::embed_image!("popup", "images/gallery_popup.png"),
132)]
133#[derive(Clone, PartialEq)]
134pub struct Popup {
135    pub(crate) theme: Option<PopupThemePartial>,
136    children: Vec<Element>,
137    on_close_request: Option<EventHandler<()>>,
138    close_on_escape_key: bool,
139    key: DiffKey,
140}
141
142impl KeyExt for Popup {
143    fn write_key(&mut self) -> &mut DiffKey {
144        &mut self.key
145    }
146}
147
148impl Default for Popup {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154impl Popup {
155    pub fn new() -> Self {
156        Self {
157            theme: None,
158            children: vec![],
159            on_close_request: None,
160            close_on_escape_key: true,
161            key: DiffKey::None,
162        }
163    }
164
165    pub fn on_close_request(mut self, on_close_request: impl Into<EventHandler<()>>) -> Self {
166        self.on_close_request = Some(on_close_request.into());
167        self
168    }
169}
170
171impl ChildrenExt for Popup {
172    fn get_children(&mut self) -> &mut Vec<Element> {
173        &mut self.children
174    }
175}
176
177impl Component for Popup {
178    fn render(&self) -> impl IntoElement {
179        let show = !self.children.is_empty();
180
181        let background_animation = use_animation_with_dependencies(&show, |conf, show| {
182            conf.on_creation(OnCreation::Finish);
183            conf.on_change(OnChange::Rerun);
184
185            let value = AnimColor::new((0, 0, 0, 0), (0, 0, 0, 150)).time(150);
186
187            if *show { value } else { value.into_reversed() }
188        });
189
190        // Depends on `show` to restart on reopen
191        let content_animation = use_animation_with_dependencies(&show, |conf, _| {
192            conf.on_creation(OnCreation::Finish);
193            conf.on_change(OnChange::Rerun);
194
195            (
196                AnimNum::new(0.85, 1.)
197                    .time(250)
198                    .ease(Ease::Out)
199                    .function(Function::Expo),
200                AnimNum::new(0.2, 1.)
201                    .time(250)
202                    .ease(Ease::Out)
203                    .function(Function::Expo),
204            )
205        });
206
207        let background_color = background_animation.get().value();
208        let should_render = show || background_color.a() > 0;
209
210        let PopupTheme {
211            background,
212            color,
213            width,
214            height,
215            padding,
216            spacing,
217        } = get_theme!(&self.theme, PopupThemePreference, "popup");
218
219        let request_to_close = {
220            let handler = self.on_close_request.clone();
221            move || {
222                if let Some(h) = &handler {
223                    h.call(());
224                }
225            }
226        };
227
228        let on_global_key_down = {
229            let close = self.close_on_escape_key;
230            let req = request_to_close.clone();
231            move |e: Event<KeyboardEventData>| {
232                if close && e.key == Key::Named(NamedKey::Escape) {
233                    req();
234                }
235            }
236        };
237
238        rect()
239            .layer(Layer::Overlay)
240            .position(Position::new_global())
241            .maybe_child(should_render.then(|| {
242                let (scale, opacity) = &*content_animation.read();
243
244                let (scale, opacity) = if show {
245                    (scale.value(), opacity.value())
246                } else {
247                    (1., 0.)
248                };
249
250                PopupBackground::new(
251                    rect()
252                        .a11y_role(AccessibilityRole::Dialog)
253                        .scale((scale, scale))
254                        .opacity(opacity)
255                        .corner_radius(12.)
256                        .background(background)
257                        .color(color)
258                        .shadow(Shadow::new().y(4.).blur(5.).color((0, 0, 0, 30)))
259                        .width(width)
260                        .height(height)
261                        .spacing(spacing)
262                        .padding(padding)
263                        .on_global_key_down(on_global_key_down)
264                        .children(self.children.clone())
265                        .into(),
266                    move |_| {
267                        request_to_close();
268                    },
269                    background_color,
270                )
271            }))
272    }
273
274    fn render_key(&self) -> DiffKey {
275        self.key.clone().or(self.default_key())
276    }
277}
278
279/// Popup title.
280#[derive(PartialEq)]
281pub struct PopupTitle {
282    text: Readable<String>,
283}
284
285impl PopupTitle {
286    pub fn new(text: impl Into<Readable<String>>) -> Self {
287        Self { text: text.into() }
288    }
289}
290
291impl Component for PopupTitle {
292    fn render(&self) -> impl IntoElement {
293        rect().font_size(18.).padding(8.).child(
294            label()
295                .a11y_role(AccessibilityRole::TitleBar)
296                .width(Size::fill())
297                .text(self.text.read().to_string()),
298        )
299    }
300}
301
302/// Popup content wrapper.
303#[derive(Clone, PartialEq)]
304pub struct PopupContent {
305    children: Vec<Element>,
306}
307impl Default for PopupContent {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl PopupContent {
314    pub fn new() -> Self {
315        Self { children: vec![] }
316    }
317}
318
319impl ChildrenExt for PopupContent {
320    fn get_children(&mut self) -> &mut Vec<Element> {
321        &mut self.children
322    }
323}
324
325impl Component for PopupContent {
326    fn render(&self) -> impl IntoElement {
327        rect()
328            .font_size(15.)
329            .padding(8.)
330            .children(self.children.clone())
331    }
332}
333
334/// Popup buttons container.
335#[derive(Clone, PartialEq)]
336pub struct PopupButtons {
337    pub children: Vec<Element>,
338}
339
340impl Default for PopupButtons {
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346impl PopupButtons {
347    pub fn new() -> Self {
348        Self { children: vec![] }
349    }
350}
351
352impl ChildrenExt for PopupButtons {
353    fn get_children(&mut self) -> &mut Vec<Element> {
354        &mut self.children
355    }
356}
357
358impl Component for PopupButtons {
359    fn render(&self) -> impl IntoElement {
360        rect()
361            .width(Size::fill())
362            .main_align(Alignment::End)
363            .padding(8.)
364            .spacing(4.)
365            .horizontal()
366            .children(self.children.clone())
367    }
368}