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 should_render = show || *background_animation.is_running().read();
208
209        let PopupTheme {
210            background,
211            color,
212            width,
213            height,
214            padding,
215            spacing,
216        } = get_theme!(&self.theme, PopupThemePreference, "popup");
217
218        let request_to_close = {
219            let handler = self.on_close_request.clone();
220            move || {
221                if let Some(h) = &handler {
222                    h.call(());
223                }
224            }
225        };
226
227        let on_global_key_down = {
228            let close = self.close_on_escape_key;
229            let req = request_to_close.clone();
230            move |e: Event<KeyboardEventData>| {
231                if close && e.key == Key::Named(NamedKey::Escape) {
232                    req();
233                }
234            }
235        };
236
237        rect()
238            .layer(Layer::Overlay)
239            .position(Position::new_global())
240            .maybe_child(should_render.then(|| {
241                let background_color = background_animation.get().value();
242
243                let (scale, opacity) = &*content_animation.read();
244
245                let (scale, opacity) = if show {
246                    (scale.value(), opacity.value())
247                } else {
248                    (1., 0.)
249                };
250
251                PopupBackground::new(
252                    rect()
253                        .a11y_role(AccessibilityRole::Dialog)
254                        .scale((scale, scale))
255                        .opacity(opacity)
256                        .corner_radius(12.)
257                        .background(background)
258                        .color(color)
259                        .shadow(Shadow::new().y(4.).blur(5.).color((0, 0, 0, 30)))
260                        .width(width)
261                        .height(height)
262                        .spacing(spacing)
263                        .padding(padding)
264                        .on_global_key_down(on_global_key_down)
265                        .children(self.children.clone())
266                        .into(),
267                    move |_| {
268                        request_to_close();
269                    },
270                    background_color,
271                )
272            }))
273    }
274
275    fn render_key(&self) -> DiffKey {
276        self.key.clone().or(self.default_key())
277    }
278}
279
280/// Popup title.
281#[derive(PartialEq)]
282pub struct PopupTitle {
283    text: Readable<String>,
284}
285
286impl PopupTitle {
287    pub fn new(text: impl Into<Readable<String>>) -> Self {
288        Self { text: text.into() }
289    }
290}
291
292impl Component for PopupTitle {
293    fn render(&self) -> impl IntoElement {
294        rect().font_size(18.).padding(8.).child(
295            label()
296                .a11y_role(AccessibilityRole::TitleBar)
297                .width(Size::fill())
298                .text(self.text.read().to_string()),
299        )
300    }
301}
302
303/// Popup content wrapper.
304#[derive(Clone, PartialEq)]
305pub struct PopupContent {
306    children: Vec<Element>,
307}
308impl Default for PopupContent {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314impl PopupContent {
315    pub fn new() -> Self {
316        Self { children: vec![] }
317    }
318}
319
320impl ChildrenExt for PopupContent {
321    fn get_children(&mut self) -> &mut Vec<Element> {
322        &mut self.children
323    }
324}
325
326impl Component for PopupContent {
327    fn render(&self) -> impl IntoElement {
328        rect()
329            .font_size(15.)
330            .padding(8.)
331            .children(self.children.clone())
332    }
333}
334
335/// Popup buttons container.
336#[derive(Clone, PartialEq)]
337pub struct PopupButtons {
338    pub children: Vec<Element>,
339}
340
341impl Default for PopupButtons {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347impl PopupButtons {
348    pub fn new() -> Self {
349        Self { children: vec![] }
350    }
351}
352
353impl ChildrenExt for PopupButtons {
354    fn get_children(&mut self) -> &mut Vec<Element> {
355        &mut self.children
356    }
357}
358
359impl Component for PopupButtons {
360    fn render(&self) -> impl IntoElement {
361        rect()
362            .width(Size::fill())
363            .main_align(Alignment::End)
364            .padding(8.)
365            .spacing(4.)
366            .horizontal()
367            .children(self.children.clone())
368    }
369}