floem/views/
toggle_button.rs

1#![deny(missing_docs)]
2//! A toggle button widget. An example can be found in [widget-gallery/button](https://github.com/lapce/floem/tree/main/examples/widget-gallery)
3//! in the floem examples.
4
5use floem_reactive::{SignalGet, SignalUpdate, create_effect};
6use peniko::Brush;
7use peniko::kurbo::{Point, Size};
8use ui_events::keyboard::{Key, KeyState, KeyboardEvent};
9use ui_events::pointer::PointerEvent;
10use winit::keyboard::NamedKey;
11
12use crate::{
13    Renderer,
14    event::EventPropagation,
15    id::ViewId,
16    prop, prop_extractor,
17    style::{self, Foreground, Style},
18    style_class,
19    unit::PxPct,
20    view::View,
21    views::Decorators,
22};
23
24/// Controls the switching behavior of the switch.
25/// The corresponding style prop is [`ToggleButtonBehavior`]
26#[derive(Debug, Clone, PartialEq)]
27pub enum ToggleHandleBehavior {
28    /// The switch foreground item will follow the position of the cursor.
29    /// The toggle event happens when the cursor passes the 50% threshold.
30    Follow,
31    /// The switch foreground item will "snap" from being toggled off/on
32    /// when the cursor passes the 50% threshold.
33    Snap,
34}
35
36impl style::StylePropValue for ToggleHandleBehavior {}
37
38prop!(pub ToggleButtonInset: PxPct {} = PxPct::Px(0.));
39prop!(pub ToggleButtonCircleRad: PxPct {} = PxPct::Pct(95.));
40prop!(pub ToggleButtonBehavior: ToggleHandleBehavior {} = ToggleHandleBehavior::Snap);
41
42prop_extractor! {
43    ToggleStyle {
44        foreground: Foreground,
45        inset: ToggleButtonInset,
46        circle_rad: ToggleButtonCircleRad,
47        switch_behavior: ToggleButtonBehavior
48    }
49}
50style_class!(
51    /// A class for styling [ToggleButton] view.
52    pub ToggleButtonClass
53);
54
55/// Represents [ToggleButton] toggle state.
56#[derive(PartialEq, Eq)]
57enum ToggleState {
58    Nothing,
59    Held,
60    Drag,
61}
62
63/// A toggle button.
64pub struct ToggleButton {
65    id: ViewId,
66    state: bool,
67    ontoggle: Option<Box<dyn Fn(bool)>>,
68    position: f32,
69    held: ToggleState,
70    width: f32,
71    radius: f32,
72    style: ToggleStyle,
73}
74
75/// A reactive toggle button.
76///
77/// When the button is toggled by clicking or dragging the widget, an update will be
78/// sent to the [`ToggleButton::on_toggle`] handler.
79///
80/// By default this toggle button has a style class of [`ToggleButtonClass`] applied
81/// with a default style provided.
82/// ### Examples
83/// ```rust
84/// # use floem::reactive::{SignalGet, SignalUpdate, RwSignal};
85/// # use floem::views::toggle_button;
86/// # use floem::prelude::{palette::css, ToggleHandleBehavior};
87/// // An example using read-write signal
88/// let state = RwSignal::new(true);
89/// let toggle = toggle_button(move || state.get())
90///     // Set action when button is toggled according to the toggle state provided.
91///     .on_toggle(move |new_state| state.set(new_state));
92///
93/// // Use toggle button specific styles to control its look and behavior
94/// let customized_toggle = toggle_button(move || state.get())
95///     .on_toggle(move |new_state| state.set(new_state))
96///     .toggle_style(|s| s
97///         // Set toggle button accent color
98///         .accent_color(css::REBECCA_PURPLE)
99///         // Set toggle button circle radius
100///         .circle_rad(5.)
101///         // Set toggle button handle color
102///         .handle_color(css::PURPLE)
103///         // Set toggle button handle inset
104///         .handle_inset(1.)
105///         // Set toggle button behavior:
106///         // - `Follow` - to follow the pointer movement
107///         // - `Snap` - to snap once pointer passed 50% treshold
108///         .behavior(ToggleHandleBehavior::Snap)
109///     );
110///```
111/// ### Reactivity
112/// This function is reactive and will reactively respond to changes.
113pub fn toggle_button(state: impl Fn() -> bool + 'static) -> ToggleButton {
114    ToggleButton::new(state)
115}
116
117impl View for ToggleButton {
118    fn id(&self) -> ViewId {
119        self.id
120    }
121
122    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
123        "Toggle Button".into()
124    }
125
126    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
127        if let Ok(state) = state.downcast::<bool>() {
128            if self.held == ToggleState::Nothing {
129                self.update_restrict_position(true);
130            }
131            self.state = *state;
132            self.id.request_layout();
133        }
134    }
135
136    fn event_before_children(
137        &mut self,
138        cx: &mut crate::context::EventCx,
139        event: &crate::event::Event,
140    ) -> EventPropagation {
141        match event {
142            crate::event::Event::Pointer(PointerEvent::Down { .. }) => {
143                cx.update_active(self.id);
144                self.held = ToggleState::Held;
145            }
146            crate::event::Event::Pointer(PointerEvent::Up { .. }) => {
147                self.id.request_layout();
148
149                // if held and pointer up. toggle the position (toggle state drag already changed the position)
150                if self.held == ToggleState::Held {
151                    if self.position > self.width / 2. {
152                        self.position = 0.;
153                    } else {
154                        self.position = self.width;
155                    }
156                }
157                // set the state based on the position of the slider
158                if self.held == ToggleState::Held {
159                    if self.state && self.position < self.width / 2. {
160                        self.state = false;
161                        if let Some(ontoggle) = &self.ontoggle {
162                            ontoggle(false);
163                        }
164                    } else if !self.state && self.position > self.width / 2. {
165                        self.state = true;
166                        if let Some(ontoggle) = &self.ontoggle {
167                            ontoggle(true);
168                        }
169                    }
170                }
171                self.held = ToggleState::Nothing;
172            }
173            crate::event::Event::Pointer(PointerEvent::Move(pu)) => {
174                let point = pu.current.logical_point();
175                if self.held == ToggleState::Held || self.held == ToggleState::Drag {
176                    self.held = ToggleState::Drag;
177                    match self.style.switch_behavior() {
178                        ToggleHandleBehavior::Follow => {
179                            self.position = point.x as f32;
180                            if self.position > self.width / 2. && !self.state {
181                                self.state = true;
182                                if let Some(ontoggle) = &self.ontoggle {
183                                    ontoggle(true);
184                                }
185                            } else if self.position < self.width / 2. && self.state {
186                                self.state = false;
187                                if let Some(ontoggle) = &self.ontoggle {
188                                    ontoggle(false);
189                                }
190                            }
191                            self.id.request_layout();
192                        }
193                        ToggleHandleBehavior::Snap => {
194                            if point.x as f32 > self.width / 2. && !self.state {
195                                self.position = self.width;
196                                self.id.request_layout();
197                                self.state = true;
198                                if let Some(ontoggle) = &self.ontoggle {
199                                    ontoggle(true);
200                                }
201                            } else if (point.x as f32) < self.width / 2. && self.state {
202                                self.position = 0.;
203                                // self.held = ToggleState::Nothing;
204                                self.id.request_layout();
205                                self.state = false;
206                                if let Some(ontoggle) = &self.ontoggle {
207                                    ontoggle(false);
208                                }
209                            }
210                        }
211                    }
212                }
213            }
214            crate::event::Event::FocusLost => {
215                self.held = ToggleState::Nothing;
216            }
217            crate::event::Event::Key(KeyboardEvent {
218                state: KeyState::Down,
219                key,
220                ..
221            }) => {
222                if *key == Key::Named(NamedKey::Enter) {
223                    if let Some(ontoggle) = &self.ontoggle {
224                        ontoggle(!self.state);
225                    }
226                }
227            }
228            _ => {}
229        }
230        EventPropagation::Continue
231    }
232
233    fn compute_layout(
234        &mut self,
235        _cx: &mut crate::context::ComputeLayoutCx,
236    ) -> Option<peniko::kurbo::Rect> {
237        let layout = self.id.get_layout().unwrap_or_default();
238        let size = layout.size;
239        self.width = size.width;
240        let circle_radius = match self.style.circle_rad() {
241            PxPct::Px(px) => px as f32,
242            PxPct::Pct(pct) => size.width.min(size.height) / 2. * (pct as f32 / 100.),
243        };
244        self.radius = circle_radius;
245        self.update_restrict_position(false);
246
247        None
248    }
249
250    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
251        if self.style.read(cx) {
252            cx.window_state.request_paint(self.id);
253        }
254    }
255
256    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
257        let layout = self.id.get_layout().unwrap_or_default();
258        let size = Size::new(layout.size.width as f64, layout.size.height as f64);
259        let circle_point = Point::new(self.position as f64, size.to_rect().center().y);
260        let circle = crate::kurbo::Circle::new(circle_point, self.radius as f64);
261        if let Some(color) = self.style.foreground() {
262            cx.fill(&circle, &color, 0.);
263        }
264    }
265}
266
267impl ToggleButton {
268    fn update_restrict_position(&mut self, end_pos: bool) {
269        let inset = match self.style.inset() {
270            PxPct::Px(px) => px as f32,
271            PxPct::Pct(pct) => (self.width * (pct as f32 / 100.)).min(self.width / 2.),
272        };
273
274        if self.held == ToggleState::Nothing || end_pos {
275            self.position = if self.state { self.width } else { 0. };
276        }
277
278        self.position = self
279            .position
280            .max(self.radius + inset)
281            .min(self.width - self.radius - inset);
282    }
283
284    /// Create new [ToggleButton].
285    ///
286    /// When the button is toggled by clicking or dragging the widget, an update will be
287    /// sent to the [`ToggleButton::on_toggle`] handler.
288    ///
289    /// By default this toggle button has a style class of [`ToggleButtonClass`] applied
290    /// with a default style provided.
291    /// ### Examples
292    /// ```rust
293    /// # use floem::reactive::{SignalGet, SignalUpdate, RwSignal};
294    /// # use floem::views::toggle_button;
295    /// # use floem::prelude::{palette::css, ToggleHandleBehavior};
296    /// // An example using read-write signal
297    /// let state = RwSignal::new(true);
298    /// let toggle = toggle_button(move || state.get())
299    ///     // Set action when button is toggled according to the toggle state provided.
300    ///     .on_toggle(move |new_state| state.set(new_state));
301    ///
302    /// // Use toggle button specific styles to control its look and behavior
303    /// let customized_toggle = toggle_button(move || state.get())
304    ///     .on_toggle(move |new_state| state.set(new_state))
305    ///     .toggle_style(|s| s
306    ///         // Set toggle button accent color
307    ///         .accent_color(css::REBECCA_PURPLE)
308    ///         // Set toggle button circle radius
309    ///         .circle_rad(5.)
310    ///         // Set toggle button handle color
311    ///         .handle_color(css::PURPLE)
312    ///         // Set toggle button handle inset
313    ///         .handle_inset(1.)
314    ///         // Set toggle button behavior:
315    ///         // - `Follow` - to follow the pointer movement
316    ///         // - `Snap` - to snap once pointer passed 50% treshold
317    ///         .behavior(ToggleHandleBehavior::Snap)
318    ///     );
319    ///```
320    /// ### Reactivity
321    /// This function is reactive and will reactively respond to changes.
322    pub fn new(state: impl Fn() -> bool + 'static) -> Self {
323        let id = ViewId::new();
324        create_effect(move |_| {
325            let state = state();
326            id.update_state(state);
327        });
328
329        Self {
330            id,
331            state: false,
332            ontoggle: None,
333            position: 0.0,
334            held: ToggleState::Nothing,
335            width: 0.,
336            radius: 0.,
337            style: Default::default(),
338        }
339        .class(ToggleButtonClass)
340    }
341
342    /// Create new [ToggleButton] with read-write signal.
343    /// ### Examples
344    /// ```rust
345    /// # use floem::prelude::*;
346    /// # use floem::prelude::palette::css;
347    /// // Create read-write signal that will hold toggle button state
348    /// let state = RwSignal::new(false);
349    /// // `.on_toggle()` is not needed as state is provided via signal
350    /// // INFO: If you use it, the state will stop updating `state` signal.
351    /// let simple = ToggleButton::new_rw(state);
352    ///
353    /// let complex = ToggleButton::new_rw(state)
354    ///     // Set styles for the toggle
355    ///     .toggle_style(move |s| s
356    ///         // Apply some styles on self optionally (here on `state` update)
357    ///         .apply_if(state.get(), |s| s
358    ///             .accent_color(css::DARK_GRAY)
359    ///             .handle_color(css::WHITE_SMOKE)
360    ///         )
361    ///         .behavior(ToggleHandleBehavior::Snap)
362    ///     );
363    /// ```
364    /// ### Reactivity
365    /// This funtion will update provided signal on toggle or will be updated if signal will change
366    /// due to external signal update.
367    pub fn new_rw(state: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static) -> Self {
368        Self::new(move || state.get()).on_toggle(move |ns| state.set(ns))
369    }
370
371    /// Add an event handler to be run when the button is toggled.
372    ///
373    /// This does not run if the state is changed because of an outside signal.
374    /// ### Rectivity
375    /// This handler is only called if this button is clicked or switched.
376    pub fn on_toggle(mut self, ontoggle: impl Fn(bool) + 'static) -> Self {
377        self.ontoggle = Some(Box::new(ontoggle));
378        self
379    }
380
381    /// Set styles related to [ToggleButton]:
382    /// - handle color
383    /// - accent color
384    /// - handle inset
385    /// - circle radius
386    /// - behavior of the switch (follow or snap)
387    pub fn toggle_style(
388        self,
389        style: impl Fn(ToggleButtonCustomStyle) -> ToggleButtonCustomStyle + 'static,
390    ) -> Self {
391        self.style(move |s| s.apply_custom(style(Default::default())))
392    }
393}
394
395/// Represents a custom style for a [ToggleButton].
396#[derive(Debug, Default, Clone)]
397pub struct ToggleButtonCustomStyle(Style);
398impl From<ToggleButtonCustomStyle> for Style {
399    fn from(value: ToggleButtonCustomStyle) -> Self {
400        value.0
401    }
402}
403
404impl ToggleButtonCustomStyle {
405    /// Create new styles for [ToggleButton].
406    pub fn new() -> Self {
407        Self(Style::new())
408    }
409
410    /// Sets the color of the toggle handle.
411    ///
412    /// # Arguments
413    /// **color** - A `Brush` that sets the handle's color.
414    pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
415        self = Self(self.0.set(Foreground, Some(color.into())));
416        self
417    }
418
419    /// Sets the accent color of the toggle button.
420    ///
421    /// # Arguments
422    /// **color** - A `Brush` that sets the toggle button's accent color.
423    /// This is the same as the background color.
424    pub fn accent_color(mut self, color: impl Into<Brush>) -> Self {
425        self = Self(self.0.background(color));
426        self
427    }
428
429    /// Sets the inset of the toggle handle.
430    ///
431    /// # Arguments
432    /// **inset** - A `PxPct` value that defines the inset of the handle from
433    /// the toggle button's edge.
434    pub fn handle_inset(mut self, inset: impl Into<PxPct>) -> Self {
435        self = Self(self.0.set(ToggleButtonInset, inset));
436        self
437    }
438
439    /// Sets the radius of the toggle circle.
440    ///
441    /// # Arguments
442    /// **rad** - A `PxPct` value that defines the radius of the toggle
443    /// button's inner circle.
444    pub fn circle_rad(mut self, rad: impl Into<PxPct>) -> Self {
445        self = Self(self.0.set(ToggleButtonCircleRad, rad));
446        self
447    }
448
449    /// Sets the switch behavior of the toggle button.
450    ///
451    /// # Arguments
452    /// **switch** - A `ToggleHandleBehavior` that defines how the toggle
453    /// handle behaves on interaction.
454    ///
455    /// On `Follow`, the handle will follow the mouse.
456    /// On `Snap`, the handle will snap to the nearest side.
457    pub fn behavior(mut self, switch: ToggleHandleBehavior) -> Self {
458        self = Self(self.0.set(ToggleButtonBehavior, switch));
459        self
460    }
461
462    /// Sets the styles of the toggle button if `true`.
463    ///
464    /// # Arguments
465    /// **cond** - if resolves to `true` will apply styles from the closure.
466    /// ```rust
467    /// # use floem::prelude::{RwSignal, palette::css};
468    /// # use crate::floem::prelude::SignalGet;
469    /// # use floem::views::ToggleButton;
470    /// let state = RwSignal::new(false);
471    /// let toggle = ToggleButton::new_rw(state)
472    ///     .toggle_style(move |s| s
473    ///         .apply_if(state.get(), |s| s
474    ///             .accent_color(css::DARK_GRAY)
475    ///         )
476    ///     );
477    /// ```
478    pub fn apply_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
479        if cond { f(self) } else { self }
480    }
481}