Skip to main content

freya_components/
popup.rs

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