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