suzy/widgets/
togglebutton.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::rc::Rc;
6
7use drying_paint::{Watched, WatchedCell};
8
9use crate::platform::{DefaultRenderPlatform, RenderPlatform};
10use crate::pointer::{PointerAction, PointerEvent};
11use crate::selectable::{Selectable, SelectionState, SelectionStateV1};
12use crate::widget::{
13    Widget, WidgetChildReceiver, WidgetContent, WidgetExtra,
14    WidgetGraphicReceiver, WidgetId, WidgetInit,
15};
16
17/// A group of toggle buttons make members of the group mutually exclusive.
18///
19/// Toggle buttons may also relate to a value, in which case the group
20/// reference can be used to retrieve the value of the currently-selected
21/// ToggleButton.
22pub struct ToggleButtonGroup<V = ()> {
23    ptr: Rc<WatchedCell<Option<V>>>,
24}
25
26impl<V> ToggleButtonGroup<V> {
27    /// Create a new toggle button group
28    pub fn new() -> Self {
29        Self {
30            ptr: Rc::new(WatchedCell::new(None)),
31        }
32    }
33
34    /// Fetch the value from the toggle group, and reset the group so that
35    /// nothing is selected.
36    ///
37    /// This will bind a current watch function to the value of the group.
38    pub fn take(&self) -> Option<V> {
39        self.ptr.take()
40    }
41
42    fn set(&self, value: V) {
43        self.ptr.set(Some(value));
44    }
45
46    fn unset(&self) {
47        self.ptr.set(None);
48    }
49
50    fn private_clone(&self) -> Self {
51        Self {
52            ptr: Rc::clone(&self.ptr),
53        }
54    }
55}
56
57impl<V: Copy> ToggleButtonGroup<V> {
58    /// Copy the value of the currently selected toggle button which belongs
59    /// to this group.
60    ///
61    /// This will bind a current watch function to the value of the group.
62    pub fn value(&self) -> Option<V> {
63        self.ptr.get()
64    }
65}
66
67impl<V> Default for ToggleButtonGroup<V> {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73/// Custom toggle button content can implement this trait to describe
74/// what value is associated with which content.
75pub trait ToggleButtonValue<V> {
76    /// Get the value associated with this toggle button content.
77    fn get_value(&self, extra: &WidgetExtra) -> V;
78}
79
80impl<T> ToggleButtonValue<()> for T {
81    fn get_value(&self, _extra: &WidgetExtra) {}
82}
83
84impl<T> ToggleButtonValue<WidgetId> for T {
85    fn get_value(&self, extra: &WidgetExtra) -> WidgetId {
86        extra.id()
87    }
88}
89
90pub struct ToggleButtonContent<T, V = ()> {
91    state: Watched<SelectionState>,
92    group: Watched<Option<ToggleButtonGroup<V>>>,
93    allow_unselect: bool,
94    interactable: Watched<bool>,
95    pointers_down: usize,
96    just_clicked: bool,
97    currently_selected: bool,
98    content: T,
99}
100
101impl<T, V> ToggleButtonContent<T, V> {
102    pub fn content(&self) -> &T {
103        &self.content
104    }
105
106    pub fn content_mut(&mut self) -> &mut T {
107        &mut self.content
108    }
109
110    pub fn state(&self) -> SelectionState {
111        *self.state
112    }
113
114    pub fn add_to_group(&mut self, group: &ToggleButtonGroup<V>) {
115        if let Some(existing) = &*self.group {
116            if Rc::ptr_eq(&existing.ptr, &group.ptr) {
117                return;
118            }
119        }
120        *self.group = Some(group.private_clone());
121        group.unset();
122    }
123
124    fn base_state(&self) -> SelectionState {
125        if self.currently_selected {
126            SelectionState::active()
127        } else {
128            SelectionState::normal()
129        }
130    }
131}
132
133impl<T, V, P> WidgetContent<P> for ToggleButtonContent<T, V>
134where
135    P: RenderPlatform,
136    T: Selectable + WidgetContent<P> + ToggleButtonValue<V>,
137    V: 'static + std::fmt::Debug + Copy,
138{
139    fn init(mut init: impl WidgetInit<Self, P>) {
140        init.init_child_inline(|button| &mut button.content);
141        init.watch(|button, _rect| {
142            button.content.selection_changed(*button.state);
143        });
144        init.watch(|button, _rect| {
145            if !*button.interactable {
146                *button.state = button.base_state();
147            }
148        });
149        init.watch(|button, _rect| {
150            if let Some(group) = &*button.group {
151                group.ptr.watched();
152                // when the group changes, reset ourselves
153                // unless we initiated the change
154                if button.just_clicked {
155                    button.just_clicked = false;
156                } else if button.currently_selected {
157                    let at_base = button.state == button.base_state();
158                    button.currently_selected = false;
159                    if at_base {
160                        *button.state = button.base_state();
161                    }
162                }
163            }
164        });
165    }
166
167    fn children(&mut self, receiver: impl WidgetChildReceiver<P>) {
168        self.content.children(receiver);
169    }
170
171    fn graphics(&mut self, receiver: impl WidgetGraphicReceiver<P>) {
172        self.content.graphics(receiver);
173    }
174
175    fn hittest(&self, extra: &mut WidgetExtra<'_>, point: (f32, f32)) -> bool {
176        self.content.hittest(extra, point)
177    }
178
179    fn pointer_event(
180        &mut self,
181        extra: &mut WidgetExtra<'_>,
182        event: &mut PointerEvent,
183    ) -> bool {
184        match event.action() {
185            PointerAction::Down => {
186                let grabbed = self.hittest(extra, event.pos())
187                    && event.try_grab(extra.id());
188                if grabbed {
189                    eprintln!("down: {:?}", self.content.get_value(&extra));
190                    self.pointers_down += 1;
191                    if *self.interactable {
192                        *self.state = SelectionState::pressed();
193                    }
194                }
195                grabbed
196            }
197            PointerAction::Move(_, _) => {
198                let ungrabbed = !self.hittest(extra, event.pos())
199                    && event.try_ungrab(extra);
200                if ungrabbed {
201                    self.pointers_down -= 1;
202                    if self.pointers_down == 0 {
203                        *self.state = self.base_state();
204                    }
205                }
206                ungrabbed
207            }
208            PointerAction::GrabStolen => {
209                self.pointers_down -= 1;
210                if self.pointers_down == 0 {
211                    *self.state = self.base_state();
212                }
213                true
214            }
215            PointerAction::Up => {
216                let ungrabbed = event.try_ungrab(extra.id());
217                if ungrabbed {
218                    self.pointers_down -= 1;
219                    if self.pointers_down == 0 {
220                        if !self.currently_selected {
221                            self.just_clicked = true;
222                            if let Some(group) = &*self.group {
223                                group.set(self.content.get_value(extra));
224                            }
225                            self.currently_selected = true;
226                        } else if self.allow_unselect {
227                            if let Some(group) = &*self.group {
228                                group.unset();
229                            }
230                            self.currently_selected = false;
231                        }
232                        *self.state = self.base_state();
233                    }
234                }
235                ungrabbed
236            }
237            PointerAction::Hover(_, _) => {
238                match (self.state.v1(), self.hittest(extra, event.pos())) {
239                    (SelectionStateV1::Normal, true) => {
240                        let grabbed = event.try_grab(extra);
241                        if grabbed && *self.interactable {
242                            *self.state = SelectionState::hover();
243                        }
244                        grabbed
245                    }
246                    (SelectionStateV1::Hover, false) => {
247                        let ungrabbed = event.try_ungrab(extra);
248                        if ungrabbed {
249                            *self.state = self.base_state();
250                        }
251                        ungrabbed
252                    }
253                    _ => false,
254                }
255            }
256            _ => false,
257        }
258    }
259}
260
261impl<T: Default, V> Default for ToggleButtonContent<T, V> {
262    fn default() -> Self {
263        Self {
264            state: Watched::default(),
265            group: Watched::new(None),
266            allow_unselect: true,
267            interactable: Watched::new(true),
268            pointers_down: 0,
269            just_clicked: false,
270            currently_selected: false,
271            content: T::default(),
272        }
273    }
274}
275
276/// A button which remains in an active state after being selected.
277pub type ToggleButton<T, V = (), P = DefaultRenderPlatform> =
278    Widget<ToggleButtonContent<T, V>, P>;