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::geom::Point;
150    use std::collections::VecDeque;
151    use std::sync::mpsc::channel;
152
153    fn create_test_context() -> Context {
154        Context::new(
155            Box::new(crate::framebuffer::Pixmap::new(600, 800, 1)),
156            None,
157            crate::library::Library::new(
158                std::path::Path::new("/tmp"),
159                crate::settings::LibraryMode::Database,
160            )
161            .unwrap(),
162            crate::settings::Settings::default(),
163            crate::font::Fonts::load_from(
164                std::path::Path::new(
165                    &std::env::var("TEST_ROOT_DIR")
166                        .expect("TEST_ROOT_DIR must be set for this test."),
167                )
168                .to_path_buf(),
169            )
170            .expect("Failed to load fonts"),
171            Box::new(crate::battery::FakeBattery::new()),
172            Box::new(crate::frontlight::LightLevels::default()),
173            Box::new(0u16),
174        )
175    }
176
177    #[test]
178    fn test_new_creates_with_label_child() {
179        let rect = rect![0, 0, 200, 50];
180        let action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
181
182        assert_eq!(action_label.children.len(), 1);
183        assert!(!action_label.active);
184    }
185
186    #[test]
187    fn test_finger_down_activates() {
188        let rect = rect![0, 0, 200, 50];
189        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
190        let (hub, _receiver) = channel();
191        let mut bus = VecDeque::new();
192        let mut rq = RenderQueue::new();
193        let mut context = create_test_context();
194
195        assert!(!action_label.active);
196
197        let point = Point::new(100, 25);
198        let event = Event::Device(DeviceEvent::Finger {
199            status: FingerStatus::Down,
200            position: point,
201            id: 0,
202            time: 0.0,
203        });
204        let handled = action_label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
205
206        assert!(handled);
207        assert!(action_label.active);
208        assert!(!rq.is_empty());
209    }
210
211    #[test]
212    fn test_finger_up_deactivates() {
213        let rect = rect![0, 0, 200, 50];
214        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
215
216        let (hub, _receiver) = channel();
217        let mut bus = VecDeque::new();
218        let mut rq = RenderQueue::new();
219
220        // Simulate active state by setting it and updating the label scheme
221        action_label.active = true;
222        if let Some(label) = action_label.children[0].downcast_mut::<Label>() {
223            label.set_scheme(TEXT_INVERTED_HARD, &mut rq);
224        }
225        rq.clear();
226        let mut context = create_test_context();
227
228        let point = Point::new(100, 25);
229        let event = Event::Device(DeviceEvent::Finger {
230            status: FingerStatus::Up,
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        assert!(!rq.is_empty());
240    }
241
242    #[test]
243    fn test_finger_down_outside_rect_ignored() {
244        let rect = rect![0, 0, 200, 50];
245        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
246        let (hub, _receiver) = channel();
247        let mut bus = VecDeque::new();
248        let mut rq = RenderQueue::new();
249        let mut context = create_test_context();
250
251        let point = Point::new(300, 100);
252        let event = Event::Device(DeviceEvent::Finger {
253            status: FingerStatus::Down,
254            position: point,
255            id: 0,
256            time: 0.0,
257        });
258        let handled = action_label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
259
260        assert!(!handled);
261        assert!(!action_label.active);
262    }
263
264    #[test]
265    fn test_update_changes_label_text() {
266        let rect = rect![0, 0, 200, 50];
267        let mut action_label = ActionLabel::new(rect, "Initial".to_string(), Align::Right(10));
268        let mut rq = RenderQueue::new();
269
270        action_label.update("Updated", &mut rq);
271
272        assert!(!rq.is_empty());
273        if let Some(label) = action_label.children[0].downcast_ref::<Label>() {
274            assert_eq!(label.rect(), &rect);
275        }
276    }
277
278    #[test]
279    fn test_event_is_emitted_on_tap() {
280        let rect = rect![0, 0, 200, 50];
281        let action_label =
282            ActionLabel::new(rect, "Test".to_string(), Align::Right(10)).event(Some(Event::Back));
283
284        let (hub, _receiver) = channel();
285        let mut bus = VecDeque::new();
286        let mut rq = RenderQueue::new();
287        let mut context = create_test_context();
288
289        let point = Point::new(100, 25);
290        let tap_event = Event::Gesture(crate::gesture::GestureEvent::Tap(point));
291
292        let mut boxed: Box<dyn View> = Box::new(action_label);
293        crate::view::handle_event(
294            boxed.as_mut(),
295            &tap_event,
296            &hub,
297            &mut bus,
298            &mut rq,
299            &mut context,
300        );
301
302        assert_eq!(bus.len(), 1);
303        assert!(matches!(bus.pop_front(), Some(Event::Back)));
304    }
305
306    #[test]
307    fn test_set_event_updates_label() {
308        let rect = rect![0, 0, 200, 50];
309        let mut action_label = ActionLabel::new(rect, "Test".to_string(), Align::Right(10));
310
311        action_label.set_event(Some(Event::Back));
312
313        let (hub, _receiver) = channel();
314        let mut bus = VecDeque::new();
315        let mut rq = RenderQueue::new();
316        let mut context = create_test_context();
317
318        let point = Point::new(100, 25);
319        let tap_event = Event::Gesture(crate::gesture::GestureEvent::Tap(point));
320
321        let mut boxed: Box<dyn View> = Box::new(action_label);
322        crate::view::handle_event(
323            boxed.as_mut(),
324            &tap_event,
325            &hub,
326            &mut bus,
327            &mut rq,
328            &mut context,
329        );
330
331        assert_eq!(bus.len(), 1);
332        assert!(matches!(bus.pop_front(), Some(Event::Back)));
333    }
334}