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