Skip to main content

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 std::{cell::RefCell, rc::Rc, time::Duration};
6
7use floem_reactive::{Effect, SignalGet, SignalUpdate};
8use peniko::Brush;
9use peniko::kurbo::{Point, Rect, Size};
10use ui_events::pointer::PointerEvent;
11
12use crate::context::Phases;
13use crate::custom_event;
14use crate::event::listener::EventListenerTrait;
15use crate::{
16    BoxTree, ElementId, Renderer,
17    context::{EventCx, PaintCx, UpdateCx},
18    easing::Linear,
19    event::{
20        DragConfig, DragEvent, DragSourceEvent, Event, EventPropagation, InteractionEvent, Phase,
21        PointerCaptureEvent, listener::UpdatePhaseLayout,
22    },
23    prop, prop_extractor,
24    style::{FontSize, Foreground, LineHeight, Style},
25    style_class,
26    unit::Length,
27    view::View,
28    view::ViewId,
29    views::Decorators,
30};
31
32prop!(pub ToggleButtonInset: Length {} = Length::Pt(0.));
33prop!(pub ToggleButtonCircleRad: Length {} = Length::Pct(95.));
34
35prop_extractor! {
36    ToggleStyle {
37        foreground: Foreground,
38        inset: ToggleButtonInset,
39        circle_rad: ToggleButtonCircleRad,
40        font_size: FontSize,
41        line_height: LineHeight,
42    }
43}
44
45style_class!(
46    /// A class for styling [ToggleButton] view.
47    pub ToggleButtonClass
48);
49
50#[derive(Clone, Copy, Debug)]
51/// Event fired when the toggle state changes
52pub struct ToggleChanged(bool);
53impl ToggleChanged {
54    fn extract_inner(&self) -> &bool {
55        &self.0
56    }
57}
58
59custom_event!(ToggleChanged, bool, ToggleChanged::extract_inner);
60
61struct Handle {
62    element_id: ElementId,
63    box_tree: Rc<RefCell<BoxTree>>,
64    position: f64,
65    parent_id: ViewId,
66    dragged: bool,
67    moved_on_down: bool,
68}
69
70impl Handle {
71    fn new(parent_id: ViewId) -> Self {
72        Self {
73            parent_id,
74            element_id: parent_id.create_child_element_id(1),
75            box_tree: parent_id.box_tree(),
76            position: 0.0,
77            dragged: false,
78            moved_on_down: false,
79        }
80    }
81
82    fn restrict(&mut self, width: f64, radius: f64, inset: f64) {
83        self.position = self
84            .position
85            .max(radius + inset)
86            .min(width - radius - inset);
87    }
88
89    fn update_bounds(&self, size: Size, radius: f64) {
90        let rect = Rect::new(
91            self.position - radius,
92            0.,
93            self.position + radius,
94            size.height,
95        );
96        let mut bt = self.box_tree.borrow_mut();
97        bt.set_local_bounds(self.element_id.0, rect);
98    }
99
100    fn snap(&mut self, state: bool, size: Size, radius: f64, inset: f64) {
101        self.position = if state { size.width } else { 0. };
102        self.restrict(size.width, radius, inset);
103        self.update_bounds(size, radius);
104    }
105
106    fn event(
107        &mut self,
108        cx: &mut EventCx,
109        state: &mut bool,
110        toggle_size: Size,
111        radius: f64,
112        inset: f64,
113    ) {
114        match &cx.event {
115            Event::Pointer(PointerEvent::Down(e)) => {
116                if let Some(pointer_id) = e.pointer.pointer_id {
117                    cx.window_state
118                        .set_pointer_capture(pointer_id, self.element_id);
119                }
120            }
121            Event::PointerCapture(PointerCaptureEvent::Gained(drag)) => {
122                self.dragged = false;
123                cx.start_drag(*drag, DragConfig::new(1., Duration::ZERO, Linear), false);
124            }
125            Event::PointerCapture(PointerCaptureEvent::Lost(_)) => {
126                let new_state = self.position >= toggle_size.width / 2.;
127                self.position = if new_state { toggle_size.width } else { 0. };
128                self.restrict(toggle_size.width, radius, inset);
129                self.update_bounds(toggle_size, radius);
130                if new_state != *state {
131                    *state = new_state;
132                }
133                cx.window_state.request_paint(self.parent_id);
134            }
135            Event::Drag(DragEvent::Source(DragSourceEvent::Move(dme))) => {
136                self.dragged = true;
137                self.position = dme.current_state.logical_point().x;
138                self.restrict(toggle_size.width, radius, inset);
139                *state = self.position >= toggle_size.width / 2.;
140                self.update_bounds(toggle_size, radius);
141                cx.window_state.request_paint(self.parent_id);
142            }
143            Event::Interaction(InteractionEvent::Click) if !self.dragged => {
144                *state = !*state;
145                self.snap(*state, toggle_size, radius, inset);
146            }
147
148            _ => {}
149        }
150    }
151
152    fn paint(&self, cx: &mut PaintCx, color: Option<Brush>, size: Size, radius: f64) {
153        let circle_point = Point::new(self.position, size.to_rect().center().y);
154        let circle = crate::kurbo::Circle::new(circle_point, radius);
155        if let Some(color) = color {
156            cx.fill(&circle, &color, 0.);
157        }
158    }
159}
160
161/// A toggle button.
162pub struct ToggleButton {
163    id: ViewId,
164    state: bool,
165    handle: Handle,
166    style: ToggleStyle,
167}
168
169/// A reactive toggle button.
170///
171/// When the button is toggled by clicking or dragging the widget, an update will be
172/// sent to the [`ToggleButton::on_toggle`] handler.
173///
174/// By default this toggle button has a style class of [`ToggleButtonClass`] applied
175/// with a default style provided.
176/// ### Examples
177/// ```rust
178/// # use floem::reactive::{SignalGet, SignalUpdate, RwSignal};
179/// # use floem::views::toggle_button;
180/// // An example using read-write signal
181/// let state = RwSignal::new(true);
182/// let toggle = toggle_button(move || state.get())
183///     .on_toggle(move |new_state| state.set(new_state));
184/// ```
185/// ### Reactivity
186/// This function is reactive and will reactively respond to changes.
187#[deprecated]
188pub fn toggle_button(state: impl Fn() -> bool + 'static) -> ToggleButton {
189    ToggleButton::new(state)
190}
191
192impl ToggleButton {
193    fn length_resolve_cx(&self) -> crate::style::FontSizeCx {
194        let font_size = self.style.font_size();
195        let line_height = match self.style.line_height() {
196            crate::text::LineHeightValue::Pt(value) => f64::from(value),
197            crate::text::LineHeightValue::Normal(value) => font_size * f64::from(value),
198        };
199        crate::style::FontSizeCx::new(font_size, line_height)
200    }
201
202    fn circle_radius(&self, size: Size) -> f64 {
203        self.style
204            .circle_rad()
205            .resolve(size.width.min(size.height) / 2.0, &self.length_resolve_cx())
206    }
207
208    fn inset(&self, width: f64) -> f64 {
209        self.style
210            .inset()
211            .resolve(width, &self.length_resolve_cx())
212            .min(width / 2.0)
213    }
214
215    fn post_layout(&mut self) {
216        let size = self.id.get_layout_rect_local().size();
217        let radius = self.circle_radius(size);
218        let inset = self.inset(size.width);
219        self.handle.restrict(size.width, radius, inset);
220        self.handle.update_bounds(size, radius);
221    }
222
223    fn snap(&mut self) {
224        let size = self.id.get_layout_rect_local().size();
225        let radius = self.circle_radius(size);
226        let inset = self.inset(size.width);
227        self.handle.snap(self.state, size, radius, inset);
228    }
229
230    /// Create new [ToggleButton].
231    ///
232    /// When the button is toggled by clicking or dragging the widget, an update will be
233    /// sent to the [`ToggleButton::on_toggle`] handler.
234    ///
235    /// By default this toggle button has a style class of [`ToggleButtonClass`] applied
236    /// with a default style provided.
237    /// ### Examples
238    /// ```rust
239    /// # use floem::reactive::{SignalGet, SignalUpdate, RwSignal};
240    /// # use floem::views::toggle_button;
241    /// // An example using read-write signal
242    /// let state = RwSignal::new(true);
243    /// let toggle = toggle_button(move || state.get())
244    ///     .on_toggle(move |new_state| state.set(new_state));
245    /// ```
246    /// ### Reactivity
247    /// This function is reactive and will reactively respond to changes.
248    pub fn new(state: impl Fn() -> bool + 'static) -> Self {
249        let id = ViewId::new();
250        id.register_listener(UpdatePhaseLayout::listener_key());
251
252        Effect::new(move |_| {
253            let state = state();
254            id.update_state(state);
255        });
256
257        Self {
258            id,
259            state: false,
260            handle: Handle::new(id),
261            style: Default::default(),
262        }
263        .class(ToggleButtonClass)
264    }
265
266    /// Create new [ToggleButton] with read-write signal.
267    /// ### Examples
268    /// ```rust
269    /// # use floem::prelude::*;
270    /// # use floem::prelude::palette::css;
271    /// // Create read-write signal that will hold toggle button state
272    /// let state = RwSignal::new(false);
273    /// let simple = ToggleButton::new_rw(state);
274    /// ```
275    /// ### Reactivity
276    /// This function will update provided signal on toggle or will be updated if signal changes
277    /// due to external signal update.
278    pub fn new_rw(state: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static) -> Self {
279        Self::new(move || state.get())
280            .on_event_stop(ToggleChanged::listener(), move |_cx, ns| state.set(*ns))
281    }
282
283    /// Add an event handler to be run when the button is toggled.
284    ///
285    /// This does not run if the state is changed because of an outside signal.
286    #[deprecated(note = "use .on_event_stop(ToggleChanged::listener(), |_, _|) directly instead")]
287    pub fn on_toggle(self, ontoggle: impl Fn(bool) + 'static) -> Self {
288        self.on_event_stop(ToggleChanged::listener(), move |_cx, e| ontoggle(*e))
289    }
290
291    /// Set styles related to [ToggleButton]:
292    /// - handle color
293    /// - accent color
294    /// - handle inset
295    /// - circle radius
296    pub fn toggle_style(
297        self,
298        style: impl Fn(ToggleButtonCustomStyle) -> ToggleButtonCustomStyle + 'static,
299    ) -> Self {
300        self.style(move |s| s.apply_custom(style(Default::default())))
301    }
302}
303
304impl View for ToggleButton {
305    fn id(&self) -> ViewId {
306        self.id
307    }
308
309    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
310        "Toggle Button".into()
311    }
312
313    fn view_style(&self) -> Option<Style> {
314        Some(Style::new().keyboard_navigable())
315    }
316
317    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn std::any::Any>) {
318        if let Ok(state) = state.downcast::<bool>() {
319            self.state = *state;
320            self.snap();
321            self.id.request_paint();
322        }
323    }
324
325    fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
326        if UpdatePhaseLayout::extract(&cx.event).is_some() {
327            self.post_layout();
328            return EventPropagation::Stop;
329        }
330
331        if cx.phase != Phase::Target {
332            return EventPropagation::Continue;
333        }
334
335        let toggle_size = self.id.get_layout_rect_local().size();
336        let radius = self.circle_radius(toggle_size);
337        let inset = self.inset(toggle_size.width);
338
339        // Click without active capture — simple toggle (pointer click or keyboard activation)
340
341        if cx.target == self.handle.element_id {
342            let old = self.state;
343            self.handle
344                .event(cx, &mut self.state, toggle_size, radius, inset);
345            if self.state != old {
346                self.id.route_event_with_caused_by(
347                    Event::new_custom(ToggleChanged(self.state)),
348                    crate::event::RouteKind::Directed {
349                        target: self.id.get_element_id(),
350                        phases: Phases::TARGET,
351                    },
352                    Some(cx.event.clone()),
353                );
354            }
355        } else {
356            // Click on the track — move handle to click position then capture
357            if let Event::Pointer(PointerEvent::Down(pbe)) = &cx.event {
358                let old_state = self.state;
359                self.handle.position = pbe.state.logical_point().x;
360                self.handle.restrict(toggle_size.width, radius, inset);
361                self.handle.update_bounds(toggle_size, radius);
362                let new_state = self.handle.position >= toggle_size.width / 2.;
363                self.handle.moved_on_down = new_state != old_state;
364                if let Some(pointer_id) = pbe.pointer.pointer_id {
365                    cx.window_state
366                        .set_pointer_capture(pointer_id, self.handle.element_id);
367                }
368                self.id.request_paint();
369            }
370            if let Event::Interaction(InteractionEvent::Click) = &cx.event {
371                if cx.triggered_by.is_some_and(|e| e.is_keyboard_trigger())
372                    || (!self.handle.dragged && !self.handle.moved_on_down)
373                {
374                    self.state = !self.state;
375                    self.id.route_event(
376                        Event::new_custom(ToggleChanged(self.state)),
377                        crate::event::RouteKind::Directed {
378                            target: self.id.get_element_id(),
379                            phases: Phases::TARGET,
380                        },
381                    );
382                    self.snap();
383                    self.id.request_paint();
384                    return EventPropagation::Stop;
385                }
386                return EventPropagation::Continue;
387            }
388        }
389
390        EventPropagation::Continue
391    }
392
393    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
394        if self.style.read(cx) {
395            cx.window_state.request_paint(self.id);
396        }
397    }
398
399    fn paint(&mut self, cx: &mut PaintCx) {
400        self.id.request_layout();
401        if cx.target_id == self.handle.element_id {
402            let size = self.id.get_layout_rect_local().size();
403            let radius = self.circle_radius(size);
404            self.handle.paint(cx, self.style.foreground(), size, radius);
405        }
406    }
407}
408
409/// Represents a custom style for a [ToggleButton].
410#[derive(Debug, Default, Clone)]
411pub struct ToggleButtonCustomStyle(Style);
412impl From<ToggleButtonCustomStyle> for Style {
413    fn from(value: ToggleButtonCustomStyle) -> Self {
414        value.0
415    }
416}
417
418impl ToggleButtonCustomStyle {
419    /// Create new styles for [ToggleButton].
420    pub fn new() -> Self {
421        Self(Style::new())
422    }
423
424    /// Sets the color of the toggle handle.
425    pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
426        self = Self(self.0.set(Foreground, Some(color.into())));
427        self
428    }
429
430    /// Sets the accent color of the toggle button (same as background color).
431    pub fn accent_color(mut self, color: impl Into<Brush>) -> Self {
432        self = Self(self.0.background(color));
433        self
434    }
435
436    /// Sets the inset of the toggle handle from the edge.
437    pub fn handle_inset(mut self, inset: impl Into<Length>) -> Self {
438        self = Self(self.0.set(ToggleButtonInset, inset));
439        self
440    }
441
442    /// Sets the radius of the toggle circle.
443    pub fn circle_rad(mut self, rad: impl Into<Length>) -> Self {
444        self = Self(self.0.set(ToggleButtonCircleRad, rad));
445        self
446    }
447
448    /// Sets the styles of the toggle button if `cond` is `true`.
449    pub fn apply_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
450        if cond { f(self) } else { self }
451    }
452}