Skip to main content

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    /// Set the hold event for the label (mutable version).
90    pub fn set_hold_event(&mut self, event: Option<Event>) {
91        self.hold_event = event;
92    }
93
94    /// Get the current text of the label.
95    pub fn text(&self) -> &str {
96        &self.text
97    }
98
99    /// Get the current color scheme of the label.
100    #[cfg(test)]
101    pub fn get_scheme(&self) -> [Color; 3] {
102        self.scheme
103    }
104}
105
106impl View for Label {
107    /// Handle events for this label.
108    ///
109    /// Processes tap and hold gestures that occur within the label's bounds.
110    /// When a tap gesture is detected and the label has an associated event,
111    /// that event is pushed to the bus and the event is marked as handled.
112    /// Similarly, when a hold gesture is detected and the label has an associated
113    /// hold event, that event is pushed to the bus.
114    ///
115    /// # ⚠️ Important Note
116    ///
117    /// **This label consumes all tap and hold gestures that occur within its bounds.**
118    /// Even if no event is configured, the gesture will still be marked as handled and
119    /// will not propagate to other views. You must explicitly set an event using
120    /// `.event()` or `.hold_event()` for gestures to be processed. If you want taps
121    /// to pass through to underlying views, you should not use this label or configure
122    /// appropriate event handlers.
123    ///
124    /// # Arguments
125    ///
126    /// * `evt` - The event to handle
127    /// * `_hub` - The event hub (unused)
128    /// * `bus` - The event bus where events are pushed
129    /// * `_rq` - The render queue (unused)
130    /// * `_context` - The application context (unused)
131    ///
132    /// # Returns
133    ///
134    /// Returns `true` if the event was handled (consumed), `false` otherwise.
135    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _hub, bus, _rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
136    fn handle_event(
137        &mut self,
138        evt: &Event,
139        _hub: &Hub,
140        bus: &mut Bus,
141        _rq: &mut RenderQueue,
142        _context: &mut Context,
143    ) -> bool {
144        match *evt {
145            Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
146                if let Some(event) = self.event.clone() {
147                    bus.push_back(event);
148                }
149
150                true
151            }
152            Event::Gesture(GestureEvent::HoldFingerShort(center, _))
153                if self.rect.includes(center) =>
154            {
155                if let Some(event) = self.hold_event.clone() {
156                    bus.push_back(event);
157                }
158
159                true
160            }
161            _ => false,
162        }
163    }
164
165    /// Render the label to the framebuffer.
166    ///
167    /// Draws the label's background rectangle and renders the text content with proper
168    /// alignment and vertical centering. The text is rendered using the normal font style
169    /// and the foreground color from the label's color scheme.
170    ///
171    /// # Arguments
172    ///
173    /// * `fb` - The framebuffer to render to
174    /// * `_rect` - The clipping region (unused)
175    /// * `fonts` - The font manager for text rendering
176    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
177    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
178        let dpi = CURRENT_DEVICE.dpi;
179
180        fb.draw_rectangle(&self.rect, self.scheme[0]);
181
182        let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
183        let x_height = font.x_heights.0 as i32;
184        let padding = font.em() as i32;
185        let max_width = self.rect.width() as i32 - padding;
186
187        let plan = font.plan(&self.text, Some(max_width), None);
188
189        let dx = self.align.offset(plan.width, self.rect.width() as i32);
190        let dy = (self.rect.height() as i32 - x_height) / 2;
191        let pt = pt!(self.rect.min.x + dx, self.rect.max.y - dy);
192
193        font.render(fb, self.scheme[1], &plan, pt);
194    }
195
196    fn resize(
197        &mut self,
198        rect: Rectangle,
199        _hub: &Hub,
200        _rq: &mut RenderQueue,
201        _context: &mut Context,
202    ) {
203        if let Some(Event::ToggleNear(_, ref mut event_rect)) = self.event.as_mut() {
204            *event_rect = rect;
205        }
206        self.rect = rect;
207    }
208
209    fn rect(&self) -> &Rectangle {
210        &self.rect
211    }
212
213    fn rect_mut(&mut self) -> &mut Rectangle {
214        &mut self.rect
215    }
216
217    fn children(&self) -> &Vec<Box<dyn View>> {
218        &self.children
219    }
220
221    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
222        &mut self.children
223    }
224
225    fn id(&self) -> Id {
226        self.id
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::context::test_helpers::create_test_context;
234    use crate::geom::Point;
235    use crate::gesture::GestureEvent;
236    use std::collections::VecDeque;
237    use std::sync::mpsc::channel;
238
239    #[test]
240    fn test_tap_with_event_emits_and_consumes() {
241        let rect = rect![0, 0, 200, 50];
242        let mut label =
243            Label::new(rect, "Test".to_string(), Align::Center).event(Some(Event::Back));
244
245        let (hub, _receiver) = channel();
246        let mut bus = VecDeque::new();
247        let mut rq = RenderQueue::new();
248        let mut context = create_test_context();
249
250        let point = Point::new(100, 25);
251        let event = Event::Gesture(GestureEvent::Tap(point));
252        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
253
254        assert!(handled);
255        assert_eq!(bus.len(), 1);
256        assert!(matches!(bus.pop_front(), Some(Event::Back)));
257    }
258
259    #[test]
260    fn test_tap_without_event_does_consume() {
261        let rect = rect![0, 0, 200, 50];
262        let mut label = Label::new(rect, "Test".to_string(), Align::Center);
263
264        let (hub, _receiver) = channel();
265        let mut bus = VecDeque::new();
266        let mut rq = RenderQueue::new();
267        let mut context = create_test_context();
268
269        let point = Point::new(100, 25);
270        let event = Event::Gesture(GestureEvent::Tap(point));
271        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
272
273        assert!(handled);
274        assert_eq!(bus.len(), 0);
275    }
276
277    #[test]
278    fn test_tap_outside_rect_ignored() {
279        let rect = rect![0, 0, 200, 50];
280        let mut label =
281            Label::new(rect, "Test".to_string(), Align::Center).event(Some(Event::Back));
282
283        let (hub, _receiver) = channel();
284        let mut bus = VecDeque::new();
285        let mut rq = RenderQueue::new();
286        let mut context = create_test_context();
287
288        let point = Point::new(300, 100);
289        let event = Event::Gesture(GestureEvent::Tap(point));
290        let handled = label.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
291
292        assert!(!handled);
293        assert_eq!(bus.len(), 0);
294    }
295}