1#![deny(missing_docs)]
2use 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 pub ToggleButtonClass
48);
49
50#[derive(Clone, Copy, Debug)]
51pub 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
161pub struct ToggleButton {
163 id: ViewId,
164 state: bool,
165 handle: Handle,
166 style: ToggleStyle,
167}
168
169#[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 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 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 #[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 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 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 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#[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 pub fn new() -> Self {
421 Self(Style::new())
422 }
423
424 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 pub fn accent_color(mut self, color: impl Into<Brush>) -> Self {
432 self = Self(self.0.background(color));
433 self
434 }
435
436 pub fn handle_inset(mut self, inset: impl Into<Length>) -> Self {
438 self = Self(self.0.set(ToggleButtonInset, inset));
439 self
440 }
441
442 pub fn circle_rad(mut self, rad: impl Into<Length>) -> Self {
444 self = Self(self.0.set(ToggleButtonCircleRad, rad));
445 self
446 }
447
448 pub fn apply_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
450 if cond { f(self) } else { self }
451 }
452}