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}