Skip to main content

freya_components/
segmented_button.rs

1use freya_core::prelude::*;
2use torin::{
3    gaps::Gaps,
4    size::Size,
5};
6
7use crate::{
8    define_theme,
9    get_theme,
10    icons::tick::TickIcon,
11};
12
13define_theme! {
14    %[component]
15    pub ButtonSegment {
16        %[fields]
17        background: Color,
18        hover_background: Color,
19        disabled_background: Color,
20        selected_background: Color,
21        focus_background: Color,
22        padding: Gaps,
23        selected_padding: Gaps,
24        width: Size,
25        height: Size,
26        color: Color,
27        selected_icon_fill: Color,
28    }
29}
30
31define_theme! {
32    %[component]
33    pub SegmentedButton {
34        %[fields]
35        background: Color,
36        border_fill: Color,
37        corner_radius: CornerRadius,
38    }
39}
40
41/// Identifies the current status of the [`ButtonSegment`]s.
42#[derive(Debug, Default, PartialEq, Clone, Copy)]
43pub enum ButtonSegmentStatus {
44    /// Default state.
45    #[default]
46    Idle,
47    /// Pointer is hovering the button.
48    Hovering,
49}
50
51/// A segment button to be used within a [`SegmentedButton`].
52///
53/// # Example
54///
55/// ```rust
56/// # use freya::prelude::*;
57/// # use std::collections::HashSet;
58/// fn app() -> impl IntoElement {
59///     let mut selected = use_state(|| HashSet::from([1]));
60///     SegmentedButton::new().children((0..2).map(|i| {
61///         ButtonSegment::new()
62///             .key(i)
63///             .selected(selected.read().contains(&i))
64///             .on_press(move |_| {
65///                 if selected.read().contains(&i) {
66///                     selected.write().remove(&i);
67///                 } else {
68///                     selected.write().insert(i);
69///                 }
70///             })
71///             .child(format!("Option {i}"))
72///             .into()
73///     }))
74/// }
75/// ```
76#[derive(Clone, PartialEq)]
77pub struct ButtonSegment {
78    pub(crate) theme: Option<ButtonSegmentThemePartial>,
79    children: Vec<Element>,
80    on_press: Option<EventHandler<Event<PressEventData>>>,
81    selected: bool,
82    enabled: bool,
83    cursor_icon: CursorIcon,
84    key: DiffKey,
85}
86
87impl Default for ButtonSegment {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl ButtonSegment {
94    pub fn new() -> Self {
95        Self {
96            theme: None,
97            children: Vec::new(),
98            on_press: None,
99            selected: false,
100            enabled: true,
101            cursor_icon: CursorIcon::default(),
102            key: DiffKey::None,
103        }
104    }
105
106    /// Get the theme override for this component.
107    pub fn get_theme(&self) -> Option<&ButtonSegmentThemePartial> {
108        self.theme.as_ref()
109    }
110
111    /// Set a theme override for this component.
112    pub fn theme(mut self, theme: ButtonSegmentThemePartial) -> Self {
113        self.theme = Some(theme);
114        self
115    }
116
117    /// Whether this segment is currently selected.
118    pub fn is_selected(&self) -> bool {
119        self.selected
120    }
121
122    pub fn selected(mut self, selected: impl Into<bool>) -> Self {
123        self.selected = selected.into();
124        self
125    }
126
127    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
128        self.enabled = enabled.into();
129        self
130    }
131
132    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
133        self.on_press = Some(on_press.into());
134        self
135    }
136
137    /// Override the cursor icon shown when hovering over this component while enabled.
138    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
139        self.cursor_icon = cursor_icon.into();
140        self
141    }
142}
143
144impl ChildrenExt for ButtonSegment {
145    fn get_children(&mut self) -> &mut Vec<Element> {
146        &mut self.children
147    }
148}
149
150impl KeyExt for ButtonSegment {
151    fn write_key(&mut self) -> &mut DiffKey {
152        &mut self.key
153    }
154}
155
156impl Component for ButtonSegment {
157    fn render(&self) -> impl IntoElement {
158        let theme = get_theme!(&self.theme, ButtonSegmentThemePreference, "button_segment");
159        let mut status = use_state(|| ButtonSegmentStatus::Idle);
160        let a11y_id = use_a11y();
161        let focus = use_focus(a11y_id);
162
163        let ButtonSegmentTheme {
164            background,
165            hover_background,
166            disabled_background,
167            selected_background,
168            focus_background,
169            padding,
170            selected_padding,
171            width,
172            height,
173            color,
174            selected_icon_fill,
175        } = theme;
176
177        let enabled = use_reactive(&self.enabled);
178        let cursor_icon = self.cursor_icon;
179        use_drop(move || {
180            if status() == ButtonSegmentStatus::Hovering && enabled() {
181                Cursor::set(CursorIcon::default());
182            }
183        });
184
185        let on_press = self.on_press.clone();
186        let on_press = move |e: Event<PressEventData>| {
187            a11y_id.request_focus();
188            if let Some(on_press) = &on_press {
189                on_press.call(e);
190            }
191        };
192
193        let on_pointer_enter = move |_| {
194            status.set(ButtonSegmentStatus::Hovering);
195            if enabled() {
196                Cursor::set(cursor_icon);
197            } else {
198                Cursor::set(CursorIcon::NotAllowed);
199            }
200        };
201
202        let on_pointer_leave = move |_| {
203            if status() == ButtonSegmentStatus::Hovering {
204                Cursor::set(CursorIcon::default());
205                status.set(ButtonSegmentStatus::Idle);
206            }
207        };
208
209        let background = match status() {
210            _ if !self.enabled => disabled_background,
211            _ if self.selected => selected_background,
212            ButtonSegmentStatus::Hovering => hover_background,
213            ButtonSegmentStatus::Idle => background,
214        };
215
216        let padding = if self.selected {
217            selected_padding
218        } else {
219            padding
220        };
221        let background = if *focus.read() == Focus::Keyboard {
222            focus_background
223        } else {
224            background
225        };
226
227        rect()
228            .a11y_id(a11y_id)
229            .a11y_focusable(self.enabled)
230            .a11y_role(AccessibilityRole::Button)
231            .maybe(self.enabled, |rect| rect.on_press(on_press))
232            .on_pointer_enter(on_pointer_enter)
233            .on_pointer_leave(on_pointer_leave)
234            .horizontal()
235            .width(width)
236            .height(height)
237            .padding(padding)
238            .overflow(Overflow::Clip)
239            .color(color.mul_if(!self.enabled, 0.9))
240            .background(background.mul_if(!self.enabled, 0.9))
241            .center()
242            .spacing(4.)
243            .maybe_child(self.selected.then(|| {
244                TickIcon::new()
245                    .fill(selected_icon_fill)
246                    .width(Size::px(12.))
247                    .height(Size::px(12.))
248            }))
249            .children(self.children.clone())
250    }
251
252    fn render_key(&self) -> DiffKey {
253        self.key.clone().or(self.default_key())
254    }
255}
256
257/// A container for grouping [`ButtonSegment`]s together.
258///
259/// # Example
260///
261/// ```rust
262/// # use freya::prelude::*;
263/// # use std::collections::HashSet;
264/// fn app() -> impl IntoElement {
265///     let mut selected = use_state(|| HashSet::from([1]));
266///     SegmentedButton::new().children((0..2).map(|i| {
267///         ButtonSegment::new()
268///             .key(i)
269///             .selected(selected.read().contains(&i))
270///             .on_press(move |_| {
271///                 if selected.read().contains(&i) {
272///                     selected.write().remove(&i);
273///                 } else {
274///                     selected.write().insert(i);
275///                 }
276///             })
277///             .child(format!("Option {i}"))
278///             .into()
279///     }))
280/// }
281/// # use freya_testing::prelude::*;
282/// # launch_doc(|| {
283/// #   rect().center().expanded().child(app())
284/// # }, "./images/gallery_segmented_button.png").render();
285/// ```
286///
287/// # Preview
288/// ![SegmentedButton Preview][segmented_button]
289#[cfg_attr(feature = "docs",
290    doc = embed_doc_image::embed_image!("segmented_button", "images/gallery_segmented_button.png")
291)]
292#[derive(Clone, PartialEq)]
293pub struct SegmentedButton {
294    pub(crate) theme: Option<SegmentedButtonThemePartial>,
295    children: Vec<Element>,
296    key: DiffKey,
297}
298
299impl Default for SegmentedButton {
300    fn default() -> Self {
301        Self::new()
302    }
303}
304
305impl SegmentedButton {
306    pub fn new() -> Self {
307        Self {
308            theme: None,
309            children: Vec::new(),
310            key: DiffKey::None,
311        }
312    }
313
314    pub fn theme(mut self, theme: SegmentedButtonThemePartial) -> Self {
315        self.theme = Some(theme);
316        self
317    }
318}
319
320impl ChildrenExt for SegmentedButton {
321    fn get_children(&mut self) -> &mut Vec<Element> {
322        &mut self.children
323    }
324}
325
326impl KeyExt for SegmentedButton {
327    fn write_key(&mut self) -> &mut DiffKey {
328        &mut self.key
329    }
330}
331
332impl Component for SegmentedButton {
333    fn render(&self) -> impl IntoElement {
334        let theme = get_theme!(
335            &self.theme,
336            SegmentedButtonThemePreference,
337            "segmented_button"
338        );
339
340        let SegmentedButtonTheme {
341            background,
342            border_fill,
343            corner_radius,
344        } = theme;
345
346        rect()
347            .overflow(Overflow::Clip)
348            .background(background)
349            .border(
350                Border::new()
351                    .fill(border_fill)
352                    .width(1.)
353                    .alignment(BorderAlignment::Outer),
354            )
355            .corner_radius(corner_radius)
356            .horizontal()
357            .children(self.children.clone())
358    }
359
360    fn render_key(&self) -> DiffKey {
361        self.key.clone().or(self.default_key())
362    }
363}