cadmus_core/view/
label.rs

1use super::{Align, Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
2use crate::color::{Color, TEXT_NORMAL};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
6use crate::framebuffer::{Framebuffer, UpdateMode};
7use crate::geom::Rectangle;
8use crate::gesture::GestureEvent;
9
10/// A text label widget that displays a single line of text.
11///
12/// `Label` is a UI component that renders text with configurable alignment and color scheme.
13/// It can optionally respond to tap and hold gestures by emitting events.
14///
15/// # Fields
16///
17/// * `id` - Unique identifier for this view
18/// * `rect` - The rectangular bounds of the label
19/// * `children` - Child views (typically empty for labels)
20/// * `text` - The text content to display
21/// * `align` - Horizontal alignment of the text (left, center, or right)
22/// * `scheme` - Color scheme as [background, foreground, border]
23/// * `event` - Optional event to emit when the label is tapped
24/// * `hold_event` - Optional event to emit when the label is held
25pub struct Label {
26    id: Id,
27    rect: Rectangle,
28    children: Vec<Box<dyn View>>,
29    text: String,
30    align: Align,
31    scheme: [Color; 3],
32    event: Option<Event>,
33    hold_event: Option<Event>,
34}
35
36impl Label {
37    pub fn new(rect: Rectangle, text: String, align: Align) -> Label {
38        Label {
39            id: ID_FEEDER.next(),
40            rect,
41            children: Vec::new(),
42            text,
43            align,
44            scheme: TEXT_NORMAL,
45            event: None,
46            hold_event: None,
47        }
48    }
49
50    /// Set the tap event for the label.
51    pub fn event(mut self, event: Option<Event>) -> Label {
52        self.event = event;
53        self
54    }
55
56    /// Set the hold event for the label.
57    pub fn hold_event(mut self, event: Option<Event>) -> Label {
58        self.hold_event = event;
59        self
60    }
61
62    /// Set the color scheme for the label.
63    pub fn scheme(mut self, scheme: [Color; 3]) -> Label {
64        self.scheme = scheme;
65        self
66    }
67
68    /// Update the text content of the label.
69    pub fn update(&mut self, text: &str, rq: &mut RenderQueue) {
70        if self.text != text {
71            self.text = text.to_string();
72            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
73        }
74    }
75
76    /// Update the color scheme of the label.
77    pub fn set_scheme(&mut self, scheme: [Color; 3], rq: &mut RenderQueue) {
78        if self.scheme != scheme {
79            self.scheme = scheme;
80            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
81        }
82    }
83
84    /// Set the tap event for the label (mutable version).
85    pub fn set_event(&mut self, event: Option<Event>) {
86        self.event = event;
87    }
88
89    /// Get the current text of the label.
90    pub fn text(&self) -> &str {
91        &self.text
92    }
93
94    /// Get the current color scheme of the label.
95    #[cfg(test)]
96    pub fn get_scheme(&self) -> [Color; 3] {
97        self.scheme
98    }
99}
100
101impl View for Label {
102    /// Handle events for this label.
103    ///
104    /// Processes tap and hold gestures that occur within the label's bounds.
105    /// When a tap gesture is detected and the label has an associated event,
106    /// that event is pushed to the bus and the event is marked as handled.
107    /// Similarly, when a hold gesture is detected and the label has an associated
108    /// hold event, that event is pushed to the bus.
109    ///
110    /// # ⚠️ Important Note
111    ///
112    /// **This label consumes all tap and hold gestures that occur within its bounds.**
113    /// Even if no event is configured, the gesture will still be marked as handled and
114    /// will not propagate to other views. You must explicitly set an event using
115    /// `.event()` or `.hold_event()` for gestures to be processed. If you want taps
116    /// to pass through to underlying views, you should not use this label or configure
117    /// appropriate event handlers.
118    ///
119    /// # Arguments
120    ///
121    /// * `evt` - The event to handle
122    /// * `_hub` - The event hub (unused)
123    /// * `bus` - The event bus where events are pushed
124    /// * `_rq` - The render queue (unused)
125    /// * `_context` - The application context (unused)
126    ///
127    /// # Returns
128    ///
129    /// Returns `true` if the event was handled (consumed), `false` otherwise.
130    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
131    fn handle_event(
132        &mut self,
133        evt: &Event,
134        _hub: &Hub,
135        bus: &mut Bus,
136        _rq: &mut RenderQueue,
137        _context: &mut Context,
138    ) -> bool {
139        match *evt {
140            Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
141                if let Some(event) = self.event.clone() {
142                    bus.push_back(event);
143                }
144
145                true
146            }
147            Event::Gesture(GestureEvent::HoldFingerShort(center, _))
148                if self.rect.includes(center) =>
149            {
150                if let Some(event) = self.hold_event.clone() {
151                    bus.push_back(event);
152                }
153
154                true
155            }
156            _ => false,
157        }
158    }
159
160    /// Render the label to the framebuffer.
161    ///
162    /// Draws the label's background rectangle and renders the text content with proper
163    /// alignment and vertical centering. The text is rendered using the normal font style
164    /// and the foreground color from the label's color scheme.
165    ///
166    /// # Arguments
167    ///
168    /// * `fb` - The framebuffer to render to
169    /// * `_rect` - The clipping region (unused)
170    /// * `fonts` - The font manager for text rendering
171    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
172    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
173        let dpi = CURRENT_DEVICE.dpi;
174
175        fb.draw_rectangle(&self.rect, self.scheme[0]);
176
177        let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
178        let x_height = font.x_heights.0 as i32;
179        let padding = font.em() as i32;
180        let max_width = self.rect.width() as i32 - padding;
181
182        let plan = font.plan(&self.text, Some(max_width), None);
183
184        let dx = self.align.offset(plan.width, self.rect.width() as i32);
185        let dy = (self.rect.height() as i32 - x_height) / 2;
186        let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy);
187
188        font.render(fb, self.scheme[1], &plan, pt);
189    }
190
191    fn resize(
192        &mut self,
193        rect: Rectangle,
194        _hub: &Hub,
195        _rq: &mut RenderQueue,
196        _context: &mut Context,
197    ) {
198        if let Some(Event::ToggleNear(_, ref mut event_rect)) = self.event.as_mut() {
199            *event_rect = rect;
200        }
201        self.rect = rect;
202    }
203
204    fn rect(&self) -> &Rectangle {
205        &self.rect
206    }
207
208    fn rect_mut(&mut self) -> &mut Rectangle {
209        &mut self.rect
210    }
211
212    fn children(&self) -> &Vec<Box<dyn View>> {
213        &self.children
214    }
215
216    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
217        &mut self.children
218    }
219
220    fn id(&self) -> Id {
221        self.id
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::context::test_helpers::create_test_context;
229    use crate::geom::Point;
230    use crate::gesture::GestureEvent;
231    use std::collections::VecDeque;
232    use std::sync::mpsc::channel;
233
234    #[test]
235    fn test_tap_with_event_emits_and_consumes() {
236        let rect = rect![0, 0, 200, 50];
237        let mut label =
238            Label::new(rect, "Test".to_string(), Align::Center).event(Some(Event::Back));
239
240        let (hub, _receiver) = channel();
241        let mut bus = VecDeque::new();
242        let mut rq = RenderQueue::new();
243        let mut context = create_test_context();
244
245        let point = Point::new(100, 25);
246        let event = Event::Gesture(GestureEvent::Tap(point));
247        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
248
249        assert!(handled);
250        assert_eq!(bus.len(), 1);
251        assert!(matches!(bus.pop_front(), Some(Event::Back)));
252    }
253
254    #[test]
255    fn test_tap_without_event_does_consume() {
256        let rect = rect![0, 0, 200, 50];
257        let mut label = Label::new(rect, "Test".to_string(), Align::Center);
258
259        let (hub, _receiver) = channel();
260        let mut bus = VecDeque::new();
261        let mut rq = RenderQueue::new();
262        let mut context = create_test_context();
263
264        let point = Point::new(100, 25);
265        let event = Event::Gesture(GestureEvent::Tap(point));
266        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
267
268        assert!(handled);
269        assert_eq!(bus.len(), 0);
270    }
271
272    #[test]
273    fn test_tap_outside_rect_ignored() {
274        let rect = rect![0, 0, 200, 50];
275        let mut label =
276            Label::new(rect, "Test".to_string(), Align::Center).event(Some(Event::Back));
277
278        let (hub, _receiver) = channel();
279        let mut bus = VecDeque::new();
280        let mut rq = RenderQueue::new();
281        let mut context = create_test_context();
282
283        let point = Point::new(300, 100);
284        let event = Event::Gesture(GestureEvent::Tap(point));
285        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
286
287        assert!(!handled);
288        assert_eq!(bus.len(), 0);
289    }
290}