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