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        min_width: Size,
26        min_height: Size,
27        max_width: Size,
28        max_height: Size,
29        padding: Gaps,
30        spacing: f32,
31    }
32}
33
34/// Popup background wrapper.
35#[derive(Clone, PartialEq)]
36pub struct PopupBackground {
37    pub children: Element,
38    pub on_press: EventHandler<Event<PressEventData>>,
39    pub background: Color,
40}
41
42impl PopupBackground {
43    pub fn new(
44        children: Element,
45        on_press: impl Into<EventHandler<Event<PressEventData>>>,
46        background: Color,
47    ) -> Self {
48        Self {
49            children,
50            on_press: on_press.into(),
51            background,
52        }
53    }
54}
55
56impl Component for PopupBackground {
57    fn render(&self) -> impl IntoElement {
58        let on_press = self.on_press.clone();
59
60        rect()
61            .child(
62                rect()
63                    .on_press(on_press)
64                    .position(Position::new_global().top(0.).left(0.))
65                    .height(Size::window_percent(100.))
66                    .width(Size::window_percent(100.))
67                    .background(self.background),
68            )
69            .child(
70                rect()
71                    .position(Position::new_global().top(0.).left(0.))
72                    .height(Size::window_percent(100.))
73                    .width(Size::window_percent(100.))
74                    .center()
75                    .child(self.children.clone()),
76            )
77    }
78}
79
80/// Floating popup / dialog.
81///
82/// The popup is shown whenever it has children, and the close animation plays
83/// when the children are removed. Conditionally attach children with
84/// [`MaybeExt::maybe`] or [`MaybeExt::map`].
85///
86/// # Example
87///
88/// ```rust
89/// # use freya::prelude::*;
90/// fn app() -> impl IntoElement {
91///     let mut show_popup = use_state(|| true);
92///
93///     rect()
94///         .child(
95///             Popup::new()
96///                 .width(Size::px(250.))
97///                 .on_close_request(move |_| show_popup.set(false))
98///                 .maybe(show_popup(), |popup| {
99///                     popup
100///                         .child(PopupTitle::new("Title".to_string()))
101///                         .child(PopupContent::new().child("Hello, World!"))
102///                         .child(
103///                             PopupButtons::new().child(
104///                                 Button::new()
105///                                     .on_press(move |_| show_popup.set(false))
106///                                     .expanded()
107///                                     .filled()
108///                                     .child("Accept"),
109///                             ),
110///                         )
111///                 }),
112///         )
113///         .child(
114///             Button::new()
115///                 .child("Open")
116///                 .on_press(move |_| show_popup.toggle()),
117///         )
118/// }
119/// ```
120///
121/// See the [interactive components demo](https://freyaui.dev/demo).
122#[doc(alias = "alert")]
123#[doc(alias = "dialog")]
124#[doc(alias = "window")]
125#[derive(Clone, PartialEq)]
126pub struct Popup {
127    pub(crate) theme: Option<PopupThemePartial>,
128    children: Vec<Element>,
129    on_close_request: Option<EventHandler<()>>,
130    close_on_escape_key: bool,
131    key: DiffKey,
132}
133
134impl KeyExt for Popup {
135    fn key_mut(&mut self) -> &mut DiffKey {
136        &mut self.key
137    }
138}
139
140impl Default for Popup {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Popup {
147    pub fn new() -> Self {
148        Self {
149            theme: None,
150            children: vec![],
151            on_close_request: None,
152            close_on_escape_key: true,
153            key: DiffKey::None,
154        }
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
163impl ChildrenExt for Popup {
164    fn children_mut(&mut self) -> &mut Vec<Element> {
165        &mut self.children
166    }
167}
168
169impl Component for Popup {
170    fn render(&self) -> impl IntoElement {
171        let show = !self.children.is_empty();
172
173        let background_animation = use_animation_with_dependencies(&show, |conf, show| {
174            conf.on_creation(OnCreation::Finish);
175            conf.on_change(OnChange::Rerun);
176
177            let value = AnimColor::new((0, 0, 0, 0), (0, 0, 0, 150)).time(150);
178
179            if *show { value } else { value.into_reversed() }
180        });
181
182        // Depends on `show` to restart on reopen
183        let content_animation = use_animation_with_dependencies(&show, |conf, _| {
184            conf.on_creation(OnCreation::Finish);
185            conf.on_change(OnChange::Rerun);
186
187            (
188                AnimNum::new(0.85, 1.)
189                    .time(250)
190                    .ease(Ease::Out)
191                    .function(Function::Expo),
192                AnimNum::new(0.2, 1.)
193                    .time(250)
194                    .ease(Ease::Out)
195                    .function(Function::Expo),
196            )
197        });
198
199        let background_color = background_animation.get().value();
200        let should_render = show || background_color.a() > 0;
201
202        let PopupTheme {
203            background,
204            color,
205            width,
206            height,
207            min_width,
208            min_height,
209            max_width,
210            max_height,
211            padding,
212            spacing,
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 (scale, opacity) = &*content_animation.read();
239
240                let (scale, opacity) = if show {
241                    (scale.value(), opacity.value())
242                } else {
243                    (1., 0.)
244                };
245
246                PopupBackground::new(
247                    rect()
248                        .a11y_role(AccessibilityRole::Dialog)
249                        .scale((scale, scale))
250                        .opacity(opacity)
251                        .corner_radius(12.)
252                        .background(background)
253                        .color(color)
254                        .shadow(Shadow::new().y(4.).blur(5.).color((0, 0, 0, 30)))
255                        .width(width)
256                        .height(height)
257                        .min_width(min_width)
258                        .min_height(min_height)
259                        .max_width(max_width)
260                        .max_height(max_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 children_mut(&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 children_mut(&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}