Skip to main content

cadmus_core/view/
action_label.rs

1use super::label::Label;
2use super::{Align, Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
3use crate::color::{TEXT_INVERTED_HARD, TEXT_NORMAL};
4use crate::context::Context;
5use crate::framebuffer::Framebuffer;
6use crate::geom::Rectangle;
7use crate::input::{DeviceEvent, FingerStatus};
8
9/// A label that provides visual feedback when touched by inverting its colors.
10///
11/// The ActionLabel responds to finger down/up events by toggling an active state,
12/// which switches between normal and inverted color schemes. It delegates text
13/// rendering to an internal Label child. When tapped, it can emit an event through
14/// the internal Label.
15pub struct ActionLabel {
16    id: Id,
17    rect: Rectangle,
18    children: Vec<Box<dyn View>>,
19    active: bool,
20}
21
22impl ActionLabel {
23    pub fn new(rect: Rectangle, text: String, align: Align) -> ActionLabel {
24        let label = Label::new(rect, text, align);
25
26        ActionLabel {
27            id: ID_FEEDER.next(),
28            rect,
29            children: vec![Box::new(label)],
30            active: false,
31        }
32    }
33
34    /// Sets the event to be emitted when the label is tapped.
35    pub fn event(mut self, event: Option<Event>) -> ActionLabel {
36        if let Some(label) = self.children[0].downcast_mut::<Label>() {
37            label.set_event(event);
38        }
39        self
40    }
41
42    /// Sets the event to be emitted when the label is tapped.
43    pub fn set_event(&mut self, event: Option<Event>) {
44        if let Some(label) = self.children[0].downcast_mut::<Label>() {
45            label.set_event(event);
46        }
47    }
48
49    /// Sets the event to be emitted when the label is held (builder style).
50    pub fn hold_event(mut self, event: Option<Event>) -> ActionLabel {
51        if let Some(label) = self.children[0].downcast_mut::<Label>() {
52            label.set_hold_event(event);
53        }
54        self
55    }
56
57    /// Sets the event to be emitted when the label is held.
58    pub fn set_hold_event(&mut self, event: Option<Event>) {
59        if let Some(label) = self.children[0].downcast_mut::<Label>() {
60            label.set_hold_event(event);
61        }
62    }
63
64    /// Updates the label's text.
65    pub fn update(&mut self, text: &str, rq: &mut RenderQueue) {
66        if let Some(label) = self.children[0].downcast_mut::<Label>() {
67            label.update(text, rq);
68        }
69    }
70
71    /// Retrieves the current text value of the label.
72    pub fn value(&self) -> String {
73        if let Some(label) = self.children[0].downcast_ref::<Label>() {
74            label.text().to_string()
75        } else {
76            String::new()
77        }
78    }
79
80    /// Updates the label's color scheme based on the active state.
81    fn update_label_scheme(&mut self, rq: &mut RenderQueue) {
82        let scheme = if self.active {
83            TEXT_INVERTED_HARD
84        } else {
85            TEXT_NORMAL
86        };
87
88        if let Some(label) = self.children[0].downcast_mut::<Label>() {
89            label.set_scheme(scheme, rq);
90        }
91    }
92}
93
94impl View for ActionLabel {
95    /// Handles finger down/up events to toggle active state and update label scheme.
96    ///
97    /// This method responds to touch input by managing the active state of the label,
98    /// which controls the visual feedback through color inversion.
99    ///
100    /// Behavior:
101    /// - **Finger Down**: If the touch position is within the label's bounds, sets the active
102    ///   state to true, inverting the label's colors to provide visual feedback.
103    /// - **Finger Up**: Deactivates the label and restores normal colors. This is handled
104    ///   regardless of whether the finger position is within the label's bounds.
105    ///
106    /// Returns true if the event was handled, false otherwise.
107    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _hub, _bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
108    fn handle_event(
109        &mut self,
110        evt: &Event,
111        _hub: &Hub,
112        _bus: &mut Bus,
113        rq: &mut RenderQueue,
114        _context: &mut Context,
115    ) -> bool {
116        match *evt {
117            Event::Device(DeviceEvent::Finger {
118                status, position, ..
119            }) => match status {
120                FingerStatus::Down if self.rect.includes(position) => {
121                    self.active = true;
122                    self.update_label_scheme(rq);
123                    true
124                }
125                FingerStatus::Up if self.active => {
126                    self.active = false;
127                    self.update_label_scheme(rq);
128                    true
129                }
130                _ => false,
131            },
132            _ => false,
133        }
134    }
135
136    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
137    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
138    }
139
140    fn rect(&self) -> &Rectangle {
141        &self.rect
142    }
143
144    fn rect_mut(&mut self) -> &mut Rectangle {
145        &mut self.rect
146    }
147
148    fn children(&self) -> &Vec<Box<dyn View>> {
149        &self.children
150    }
151
152    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
153        &mut self.children
154    }
155
156    fn id(&self) -> Id {
157        self.id
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::context::test_helpers::create_test_context;
165    use crate::geom::Point;
166    use std::collections::VecDeque;
167    use std::sync::mpsc::channel;
168
169    #[test]
170    fn test_new_creates_with_label_child() {
171        let rect = rect![0, 0, 200, 50];
172        let action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
173
174        assert_eq!(action_label.children.len(), 1);
175        assert!(!action_label.active);
176    }
177
178    #[test]
179    fn test_finger_down_activates() {
180        let rect = rect![0, 0, 200, 50];
181        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
182        let (hub, _receiver) = channel();
183        let mut bus = VecDeque::new();
184        let mut rq = RenderQueue::new();
185        let mut context = create_test_context();
186
187        assert!(!action_label.active);
188
189        let point = Point::new(100, 25);
190        let event = Event::Device(DeviceEvent::Finger {
191            status: FingerStatus::Down,
192            position: point,
193            id: 0,
194            time: 0.0,
195        });
196        let handled = action_label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
197
198        assert!(handled);
199        assert!(action_label.active);
200        assert!(!rq.is_empty());
201    }
202
203    #[test]
204    fn test_finger_up_deactivates() {
205        let rect = rect![0, 0, 200, 50];
206        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
207
208        let (hub, _receiver) = channel();
209        let mut bus = VecDeque::new();
210        let mut rq = RenderQueue::new();
211
212        // Simulate active state by setting it and updating the label scheme
213        action_label.active = true;
214        if let Some(label) = action_label.children[0].downcast_mut::<Label>() {
215            label.set_scheme(TEXT_INVERTED_HARD, &mut rq);
216        }
217        rq.clear();
218        let mut context = create_test_context();
219
220        let point = Point::new(100, 25);
221        let event = Event::Device(DeviceEvent::Finger {
222            status: FingerStatus::Up,
223            position: point,
224            id: 0,
225            time: 0.0,
226        });
227        let handled = action_label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
228
229        assert!(handled);
230        assert!(!action_label.active);
231        assert!(!rq.is_empty());
232    }
233
234    #[test]
235    fn test_finger_down_outside_rect_ignored() {
236        let rect = rect![0, 0, 200, 50];
237        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
238        let (hub, _receiver) = channel();
239        let mut bus = VecDeque::new();
240        let mut rq = RenderQueue::new();
241        let mut context = create_test_context();
242
243        let point = Point::new(300, 100);
244        let event = Event::Device(DeviceEvent::Finger {
245            status: FingerStatus::Down,
246            position: point,
247            id: 0,
248            time: 0.0,
249        });
250        let handled = action_label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
251
252        assert!(!handled);
253        assert!(!action_label.active);
254    }
255
256    #[test]
257    fn test_update_changes_label_text() {
258        let rect = rect![0, 0, 200, 50];
259        let mut action_label = ActionLabel::new(rect, "Initial".to_string(), Align::Right(10));
260        let mut rq = RenderQueue::new();
261
262        action_label.update("Updated", &mut rq);
263
264        assert!(!rq.is_empty());
265        if let Some(label) = action_label.children[0].downcast_ref::<Label>() {
266            assert_eq!(label.rect(), &rect);
267        }
268    }
269
270    #[test]
271    fn test_event_is_emitted_on_tap() {
272        let rect = rect![0, 0, 200, 50];
273        let action_label =
274            ActionLabel::new(rect, "Test".to_string(), Align::Right(10)).event(Some(Event::Back));
275
276        let (hub, _receiver) = channel();
277        let mut bus = VecDeque::new();
278        let mut rq = RenderQueue::new();
279        let mut context = create_test_context();
280
281        let point = Point::new(100, 25);
282        let tap_event = Event::Gesture(crate::gesture::GestureEvent::Tap(point));
283
284        let mut boxed: Box<dyn View> = Box::new(action_label);
285        crate::view::handle_event(
286            boxed.as_mut(),
287            &tap_event,
288            &hub,
289            &mut bus,
290            &mut rq,
291            &mut context,
292        );
293
294        assert_eq!(bus.len(), 1);
295        assert!(matches!(bus.pop_front(), Some(Event::Back)));
296    }
297
298    #[test]
299    fn test_set_event_updates_label() {
300        let rect = rect![0, 0, 200, 50];
301        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
302
303        action_label.set_event(Some(Event::Back));
304
305        let (hub, _receiver) = channel();
306        let mut bus = VecDeque::new();
307        let mut rq = RenderQueue::new();
308        let mut context = create_test_context();
309
310        let point = Point::new(100, 25);
311        let tap_event = Event::Gesture(crate::gesture::GestureEvent::Tap(point));
312
313        let mut boxed: Box<dyn View> = Box::new(action_label);
314        crate::view::handle_event(
315            boxed.as_mut(),
316            &tap_event,
317            &hub,
318            &mut bus,
319            &mut rq,
320            &mut context,
321        );
322
323        assert_eq!(bus.len(), 1);
324        assert!(matches!(bus.pop_front(), Some(Event::Back)));
325    }
326}