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/// # Example
79///
80/// ```rust
81/// # use freya::prelude::*;
82/// fn app() -> impl IntoElement {
83///     let mut show_popup = use_state(|| true);
84///
85///     rect()
86///         .child(
87///             Popup::new()
88///                 .show(show_popup())
89///                 .width(Size::px(250.))
90///                 .on_close_request(move |_| show_popup.set(false))
91///                 .child(PopupTitle::new("Title".to_string()))
92///                 .child(PopupContent::new().child("Hello, World!"))
93///                 .child(
94///                     PopupButtons::new().child(
95///                         Button::new()
96///                             .on_press(move |_| show_popup.set(false))
97///                             .expanded()
98///                             .filled()
99///                             .child("Accept"),
100///                     ),
101///                 ),
102///         )
103///         .child(
104///             Button::new()
105///                 .child("Open")
106///                 .on_press(move |_| show_popup.toggle()),
107///         )
108/// }
109/// # use freya_testing::prelude::*;
110/// # launch_doc(|| {
111/// #   rect().center().expanded().child(
112/// #      app()
113/// #   )
114/// # }, "./images/gallery_popup.png").with_scale_factor(0.8).with_hook(|test| {
115/// #   test.poll(std::time::Duration::from_millis(10), std::time::Duration::from_millis(500));
116/// # }).render();
117/// ```
118///
119/// # Preview
120/// ![Popup Preview][popup]
121#[doc(alias = "alert")]
122#[doc(alias = "dialog")]
123#[doc(alias = "window")]
124#[cfg_attr(feature = "docs",
125    doc = embed_doc_image::embed_image!("popup", "images/gallery_popup.png"),
126)]
127#[derive(Clone, PartialEq)]
128pub struct Popup {
129    pub(crate) theme: Option<PopupThemePartial>,
130    children: Vec<Element>,
131    show: Readable<bool>,
132    on_close_request: Option<EventHandler<()>>,
133    close_on_escape_key: bool,
134    key: DiffKey,
135}
136
137impl KeyExt for Popup {
138    fn write_key(&mut self) -> &mut DiffKey {
139        &mut self.key
140    }
141}
142
143impl Default for Popup {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl Popup {
150    pub fn new() -> Self {
151        Self {
152            theme: None,
153            children: vec![],
154            show: true.into(),
155            on_close_request: None,
156            close_on_escape_key: true,
157            key: DiffKey::None,
158        }
159    }
160
161    pub fn show(mut self, show: impl Into<Readable<bool>>) -> Self {
162        self.show = show.into();
163        self
164    }
165
166    pub fn on_close_request(mut self, on_close_request: impl Into<EventHandler<()>>) -> Self {
167        self.on_close_request = Some(on_close_request.into());
168        self
169    }
170}
171
172impl ChildrenExt for Popup {
173    fn get_children(&mut self) -> &mut Vec<Element> {
174        &mut self.children
175    }
176}
177
178impl Component for Popup {
179    fn render(&self) -> impl IntoElement {
180        let show = *self.show.read();
181
182        let background_animation = use_animation_with_dependencies(&show, |conf, show| {
183            conf.on_creation(OnCreation::Finish);
184            conf.on_change(OnChange::Rerun);
185
186            let value = AnimColor::new((0, 0, 0, 0), (0, 0, 0, 150)).time(150);
187
188            if *show { value } else { value.into_reversed() }
189        });
190
191        // Depends on `show` to restart on reopen
192        let content_animation = use_animation_with_dependencies(&show, |conf, _| {
193            conf.on_creation(OnCreation::Finish);
194            conf.on_change(OnChange::Rerun);
195
196            (
197                AnimNum::new(0.85, 1.)
198                    .time(250)
199                    .ease(Ease::Out)
200                    .function(Function::Expo),
201                AnimNum::new(0.2, 1.)
202                    .time(250)
203                    .ease(Ease::Out)
204                    .function(Function::Expo),
205            )
206        });
207
208        let should_render = show || *background_animation.is_running().read();
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 background_color = background_animation.get().value();
243
244                let (scale, opacity) = &*content_animation.read();
245
246                let (scale, opacity) = if show {
247                    (scale.value(), opacity.value())
248                } else {
249                    (1., 0.)
250                };
251
252                PopupBackground::new(
253                    rect()
254                        .a11y_role(AccessibilityRole::Dialog)
255                        .scale((scale, scale))
256                        .opacity(opacity)
257                        .corner_radius(12.)
258                        .background(background)
259                        .color(color)
260                        .shadow(Shadow::new().y(4.).blur(5.).color((0, 0, 0, 30)))
261                        .width(width)
262                        .height(height)
263                        .spacing(spacing)
264                        .padding(padding)
265                        .on_global_key_down(on_global_key_down)
266                        .children(self.children.clone())
267                        .into(),
268                    move |_| {
269                        request_to_close();
270                    },
271                    background_color,
272                )
273            }))
274    }
275
276    fn render_key(&self) -> DiffKey {
277        self.key.clone().or(self.default_key())
278    }
279}
280
281/// Popup title.
282#[derive(PartialEq)]
283pub struct PopupTitle {
284    text: Readable<String>,
285}
286
287impl PopupTitle {
288    pub fn new(text: impl Into<Readable<String>>) -> Self {
289        Self { text: text.into() }
290    }
291}
292
293impl Component for PopupTitle {
294    fn render(&self) -> impl IntoElement {
295        rect().font_size(18.).padding(8.).child(
296            label()
297                .a11y_role(AccessibilityRole::TitleBar)
298                .width(Size::fill())
299                .text(self.text.read().to_string()),
300        )
301    }
302}
303
304/// Popup content wrapper.
305#[derive(Clone, PartialEq)]
306pub struct PopupContent {
307    children: Vec<Element>,
308}
309impl Default for PopupContent {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl PopupContent {
316    pub fn new() -> Self {
317        Self { children: vec![] }
318    }
319}
320
321impl ChildrenExt for PopupContent {
322    fn get_children(&mut self) -> &mut Vec<Element> {
323        &mut self.children
324    }
325}
326
327impl Component for PopupContent {
328    fn render(&self) -> impl IntoElement {
329        rect()
330            .font_size(15.)
331            .padding(8.)
332            .children(self.children.clone())
333    }
334}
335
336/// Popup buttons container.
337#[derive(Clone, PartialEq)]
338pub struct PopupButtons {
339    pub children: Vec<Element>,
340}
341
342impl Default for PopupButtons {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348impl PopupButtons {
349    pub fn new() -> Self {
350        Self { children: vec![] }
351    }
352}
353
354impl ChildrenExt for PopupButtons {
355    fn get_children(&mut self) -> &mut Vec<Element> {
356        &mut self.children
357    }
358}
359
360impl Component for PopupButtons {
361    fn render(&self) -> impl IntoElement {
362        rect()
363            .width(Size::fill())
364            .main_align(Alignment::End)
365            .padding(8.)
366            .spacing(4.)
367            .horizontal()
368            .children(self.children.clone())
369    }
370}