floem/views/
label.rs

1use std::{any::Any, fmt::Display, mem::swap};
2
3use crate::{
4    context::{PaintCx, UpdateCx},
5    event::{Event, EventListener, EventPropagation},
6    id::ViewId,
7    keyboard::KeyEvent,
8    prop_extractor,
9    style::{
10        CursorColor, CustomStylable, CustomStyle, FontProps, LineHeight, Selectable,
11        SelectionCornerRadius, SelectionStyle, Style, TextColor, TextOverflow, TextOverflowProp,
12    },
13    style_class,
14    text::{Attrs, AttrsList, FamilyOwned, TextLayout},
15    unit::PxPct,
16    view::View,
17    Clipboard,
18};
19use floem_reactive::create_updater;
20use floem_renderer::{text::Cursor, Renderer};
21use peniko::{
22    color::palette,
23    kurbo::{Point, Rect},
24    Brush,
25};
26use taffy::tree::NodeId;
27use winit::keyboard::{Key, SmolStr};
28
29use super::{Decorators, TextCommand};
30
31prop_extractor! {
32    Extractor {
33        color: TextColor,
34        text_overflow: TextOverflowProp,
35        line_height: LineHeight,
36        text_selectable: Selectable,
37    }
38}
39
40style_class!(pub LabelClass);
41
42struct TextOverflowListener {
43    last_is_overflown: Option<bool>,
44    on_change_fn: Box<dyn Fn(bool) + 'static>,
45}
46
47#[derive(Debug, Clone)]
48enum SelectionState {
49    None,
50    Ready(Point),
51    Selecting(Point, Point),
52    Selected(Point, Point),
53}
54
55/// A View that can display text from a [`String`]. See [`label`], [`text`], and [`static_label`].
56pub struct Label {
57    id: ViewId,
58    label: String,
59    text_layout: Option<TextLayout>,
60    text_node: Option<NodeId>,
61    available_text: Option<String>,
62    available_width: Option<f32>,
63    available_text_layout: Option<TextLayout>,
64    text_overflow_listener: Option<TextOverflowListener>,
65    selection_state: SelectionState,
66    selection_range: Option<(Cursor, Cursor)>,
67    selection_style: SelectionStyle,
68    font: FontProps,
69    style: Extractor,
70}
71
72impl Label {
73    fn new(id: ViewId, label: String) -> Self {
74        Label {
75            id,
76            label,
77            text_layout: None,
78            text_node: None,
79            available_text: None,
80            available_width: None,
81            available_text_layout: None,
82            text_overflow_listener: None,
83            selection_state: SelectionState::None,
84            selection_range: None,
85            selection_style: Default::default(),
86            font: FontProps::default(),
87            style: Default::default(),
88        }
89        .class(LabelClass)
90    }
91
92    fn effectve_text_layout(&self) -> &TextLayout {
93        self.available_text_layout
94            .as_ref()
95            .unwrap_or_else(|| self.text_layout.as_ref().unwrap())
96    }
97}
98
99/// A non-reactive view that can display text from an item that implements [`Display`]. See also [`label`].
100///
101/// ## Example
102/// ```rust
103/// use floem::views::*;
104///
105/// stack((
106///    text("non-reactive-text"),
107///    text(505),
108/// ));
109/// ```
110pub fn text<S: Display>(text: S) -> Label {
111    static_label(text.to_string())
112}
113
114/// A non-reactive view that can display text from an item that can be turned into a [`String`]. See also [`label`].
115pub fn static_label(label: impl Into<String>) -> Label {
116    Label::new(ViewId::new(), label.into())
117}
118
119/// A view that can reactively display text from an item that implements [`Display`]. See also [`text`] for a non-reactive label.
120///
121/// ## Example
122/// ```rust
123/// use floem::{reactive::*, views::*};
124///
125/// let text = RwSignal::new("Reactive text to be displayed".to_string());
126///
127/// label(move || text.get());
128/// ```
129pub fn label<S: Display + 'static>(label: impl Fn() -> S + 'static) -> Label {
130    let id = ViewId::new();
131    let initial_label = create_updater(
132        move || label().to_string(),
133        move |new_label| id.update_state(new_label),
134    );
135    Label::new(id, initial_label).on_event_cont(EventListener::FocusLost, move |_| {
136        id.request_layout();
137    })
138}
139
140impl Label {
141    pub fn on_text_overflow(mut self, is_text_overflown_fn: impl Fn(bool) + 'static) -> Self {
142        self.text_overflow_listener = Some(TextOverflowListener {
143            on_change_fn: Box::new(is_text_overflown_fn),
144            last_is_overflown: None,
145        });
146        self
147    }
148
149    fn get_attrs_list(&self) -> AttrsList {
150        let mut attrs = Attrs::new().color(self.style.color().unwrap_or(palette::css::BLACK));
151        if let Some(font_size) = self.font.size() {
152            attrs = attrs.font_size(font_size);
153        }
154        if let Some(font_style) = self.font.style() {
155            attrs = attrs.style(font_style);
156        }
157        let font_family = self.font.family().as_ref().map(|font_family| {
158            let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
159            family
160        });
161        if let Some(font_family) = font_family.as_ref() {
162            attrs = attrs.family(font_family);
163        }
164        if let Some(font_weight) = self.font.weight() {
165            attrs = attrs.weight(font_weight);
166        }
167        if let Some(line_height) = self.style.line_height() {
168            attrs = attrs.line_height(line_height);
169        }
170        AttrsList::new(attrs)
171    }
172
173    fn set_text_layout(&mut self) {
174        let mut text_layout = TextLayout::new();
175        let attrs_list = self.get_attrs_list();
176        text_layout.set_text(self.label.as_str(), attrs_list.clone());
177        self.text_layout = Some(text_layout);
178
179        if let Some(new_text) = self.available_text.as_ref() {
180            let mut text_layout = TextLayout::new();
181            text_layout.set_text(new_text, attrs_list);
182            self.available_text_layout = Some(text_layout);
183        }
184    }
185
186    fn get_hit_point(&self, point: Point) -> Option<Cursor> {
187        let text_node = self.text_node?;
188        let location = self
189            .id
190            .taffy()
191            .borrow()
192            .layout(text_node)
193            .map_or(taffy::Layout::new().location, |layout| layout.location);
194        self.effectve_text_layout().hit(
195            point.x as f32 - location.x,
196            // TODO: prevent cursor incorrectly going to end of buffer when clicking
197            // slightly below the text
198            point.y as f32 - location.y,
199        )
200    }
201
202    fn set_selection_range(&mut self) {
203        match self.selection_state {
204            SelectionState::None => {
205                self.selection_range = None;
206            }
207            SelectionState::Selecting(start, end) | SelectionState::Selected(start, end) => {
208                let mut start_cursor = self.get_hit_point(start).expect("Start position is valid");
209                if let Some(mut end_cursor) = self.get_hit_point(end) {
210                    if start_cursor.line > end_cursor.line
211                        || (start_cursor.line == end_cursor.line
212                            && start_cursor.index > end_cursor.index)
213                    {
214                        swap(&mut start_cursor, &mut end_cursor);
215                    }
216
217                    self.selection_range = Some((start_cursor, end_cursor));
218                }
219            }
220            _ => {}
221        }
222    }
223
224    fn handle_modifier_cmd(&mut self, event: &KeyEvent, character: &SmolStr) -> bool {
225        if event.modifiers.is_empty() {
226            return false;
227        }
228
229        let command = (event, character).into();
230
231        match command {
232            TextCommand::Copy => {
233                if let Some((start_c, end_c)) = &self.selection_range {
234                    if let Some(ref text_layout) = self.text_layout {
235                        let start_line_idx = text_layout.lines_range()[start_c.line].start;
236                        let end_line_idx = text_layout.lines_range()[end_c.line].start;
237                        let start_idx = start_line_idx + start_c.index;
238                        let end_idx = end_line_idx + end_c.index;
239                        let selection_txt = self.label[start_idx..end_idx].into();
240                        let _ = Clipboard::set_contents(selection_txt);
241                    }
242                }
243                true
244            }
245            _ => false,
246        }
247    }
248    fn handle_key_down(&mut self, event: &KeyEvent) -> bool {
249        match event.key.logical_key {
250            Key::Character(ref ch) => self.handle_modifier_cmd(event, ch),
251            _ => false,
252        }
253    }
254
255    fn paint_selection(&self, text_layout: &TextLayout, paint_cx: &mut PaintCx) {
256        if let Some((start_c, end_c)) = &self.selection_range {
257            let location = self
258                .id
259                .taffy()
260                .borrow()
261                .layout(self.text_node.unwrap())
262                .cloned()
263                .unwrap_or_default()
264                .location;
265            let ss = &self.selection_style;
266            let selection_color = ss.selection_color();
267
268            for run in text_layout.layout_runs() {
269                if let Some((mut start_x, width)) = run.highlight(*start_c, *end_c) {
270                    start_x += location.x;
271                    let end_x = width + start_x;
272                    let start_y = location.y as f64 + run.line_top as f64;
273                    let end_y = start_y + run.line_height as f64;
274                    let rect = Rect::new(start_x.into(), start_y, end_x.into(), end_y)
275                        .to_rounded_rect(ss.corner_radius());
276                    paint_cx.fill(&rect, &selection_color, 0.0);
277                }
278            }
279        }
280    }
281
282    pub fn label_style(
283        self,
284        style: impl Fn(LabelCustomStyle) -> LabelCustomStyle + 'static,
285    ) -> Self {
286        self.custom_style(style)
287    }
288}
289
290impl View for Label {
291    fn id(&self) -> ViewId {
292        self.id
293    }
294
295    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
296        format!("Label: {:?}", self.label).into()
297    }
298
299    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn Any>) {
300        if let Ok(state) = state.downcast() {
301            self.label = *state;
302            self.text_layout = None;
303            self.available_text = None;
304            self.available_width = None;
305            self.available_text_layout = None;
306            self.id.request_layout();
307        }
308    }
309
310    fn event_before_children(
311        &mut self,
312        _cx: &mut crate::context::EventCx,
313        event: &Event,
314    ) -> crate::event::EventPropagation {
315        match event {
316            Event::PointerDown(pe) => {
317                if self.style.text_selectable() {
318                    self.selection_range = None;
319                    self.selection_state = SelectionState::Ready(pe.pos);
320                    self.id.request_layout();
321                }
322            }
323            Event::PointerMove(pme) => {
324                if !self.style.text_selectable() {
325                    if self.selection_range.is_some() {
326                        self.selection_state = SelectionState::None;
327                        self.selection_range = None;
328                        self.id.request_layout();
329                    }
330                } else {
331                    let (SelectionState::Selecting(start, _) | SelectionState::Ready(start)) =
332                        self.selection_state
333                    else {
334                        return EventPropagation::Continue;
335                    };
336                    // this check is here to make it so that text selection doesn't eat pointer events on very small move events
337                    if start.distance(pme.pos).abs() > 2. {
338                        self.selection_state = SelectionState::Selecting(start, pme.pos);
339                        self.id.request_active();
340                        self.id.request_focus();
341                        self.id.request_layout();
342                    }
343                }
344            }
345            Event::PointerUp(_) => {
346                if let SelectionState::Selecting(start, end) = self.selection_state {
347                    self.selection_state = SelectionState::Selected(start, end);
348                } else {
349                    self.selection_state = SelectionState::None;
350                }
351                self.id.clear_active();
352                self.id.request_layout();
353            }
354            Event::KeyDown(ke) => {
355                if self.handle_key_down(ke) {
356                    return EventPropagation::Stop;
357                }
358            }
359            _ => {}
360        }
361        EventPropagation::Continue
362    }
363
364    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
365        if self.font.read(cx) | self.style.read(cx) {
366            self.text_layout = None;
367            self.available_text = None;
368            self.available_width = None;
369            self.available_text_layout = None;
370            self.id.request_layout();
371        }
372        if self.selection_style.read(cx) {
373            self.id.request_paint();
374        }
375    }
376
377    fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::tree::NodeId {
378        cx.layout_node(self.id(), true, |_cx| {
379            let (width, height) = if self.label.is_empty() {
380                (0.0, self.font.size().unwrap_or(14.0))
381            } else {
382                if self.text_layout.is_none() {
383                    self.set_text_layout();
384                }
385                let text_layout = self.text_layout.as_ref().unwrap();
386                let size = text_layout.size();
387                let width = size.width.ceil() as f32;
388                let mut height = size.height as f32;
389
390                if self.style.text_overflow() == TextOverflow::Wrap {
391                    if let Some(t) = self.available_text_layout.as_ref() {
392                        height = height.max(t.size().height as f32);
393                    }
394                }
395
396                (width, height)
397            };
398
399            if self.text_node.is_none() {
400                self.text_node = Some(
401                    self.id
402                        .taffy()
403                        .borrow_mut()
404                        .new_leaf(taffy::style::Style::DEFAULT)
405                        .unwrap(),
406                );
407            }
408            let text_node = self.text_node.unwrap();
409
410            let style = Style::new().width(width).height(height).to_taffy_style();
411            let _ = self.id.taffy().borrow_mut().set_style(text_node, style);
412
413            vec![text_node]
414        })
415    }
416
417    fn compute_layout(&mut self, _cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
418        if self.label.is_empty() {
419            return None;
420        }
421
422        let layout = self.id.get_layout().unwrap_or_default();
423        let (text_overflow, padding) = {
424            let view_state = self.id.state();
425            let view_state = view_state.borrow();
426            let style = view_state.combined_style.builtin();
427            let padding_left = match style.padding_left() {
428                PxPct::Px(padding) => padding as f32,
429                PxPct::Pct(pct) => (pct / 100.) as f32 * layout.size.width,
430            };
431            let padding_right = match style.padding_right() {
432                PxPct::Px(padding) => padding as f32,
433                PxPct::Pct(pct) => (pct / 100.) as f32 * layout.size.width,
434            };
435            let text_overflow = style.text_overflow();
436            (text_overflow, padding_left + padding_right)
437        };
438        let text_layout = self.text_layout.as_ref().unwrap();
439        let width = text_layout.size().width as f32;
440        let available_width = layout.size.width - padding;
441        if text_overflow == TextOverflow::Ellipsis {
442            if width > available_width {
443                if self.available_width != Some(available_width) {
444                    let mut dots_text = TextLayout::new();
445                    dots_text.set_text("...", self.get_attrs_list());
446
447                    let dots_width = dots_text.size().width as f32;
448                    let width_left = available_width - dots_width;
449                    let hit_point = text_layout.hit_point(Point::new(width_left as f64, 0.0));
450                    let index = hit_point.index;
451
452                    let new_text = if index > 0 {
453                        format!("{}...", &self.label[..index])
454                    } else {
455                        "".to_string()
456                    };
457                    self.available_text = Some(new_text);
458                    self.available_width = Some(available_width);
459                    self.set_text_layout();
460                }
461            } else {
462                self.available_text = None;
463                self.available_width = None;
464                self.available_text_layout = None;
465            }
466        } else if text_overflow == TextOverflow::Wrap {
467            if width > available_width {
468                if self.available_width != Some(available_width) {
469                    let mut text_layout = text_layout.clone();
470                    text_layout.set_size(available_width, f32::MAX);
471                    self.available_text_layout = Some(text_layout);
472                    self.available_width = Some(available_width);
473                    self.id.request_layout();
474                }
475            } else {
476                if self.available_text_layout.is_some() {
477                    self.id.request_layout();
478                }
479                self.available_text = None;
480                self.available_width = None;
481                self.available_text_layout = None;
482            }
483        }
484
485        self.set_selection_range();
486
487        if let Some(listener) = self.text_overflow_listener.as_mut() {
488            let was_overflown = listener.last_is_overflown;
489            let now_overflown = width > available_width;
490
491            if was_overflown != Some(now_overflown) {
492                (listener.on_change_fn)(now_overflown);
493                listener.last_is_overflown = Some(now_overflown);
494            }
495        }
496        None
497    }
498
499    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
500        if self.label.is_empty() {
501            return;
502        }
503
504        let text_node = self.text_node.unwrap();
505        let location = self
506            .id
507            .taffy()
508            .borrow()
509            .layout(text_node)
510            .map_or(taffy::Layout::new().location, |layout| layout.location);
511
512        let point = Point::new(location.x as f64, location.y as f64);
513
514        let text_layout = self.effectve_text_layout();
515        cx.draw_text(text_layout, point);
516        if cx.app_state.is_focused(&self.id()) {
517            self.paint_selection(text_layout, cx);
518        }
519    }
520}
521
522/// Represents a custom style for a `Label`.
523#[derive(Debug, Clone)]
524pub struct LabelCustomStyle(Style);
525impl From<LabelCustomStyle> for Style {
526    fn from(value: LabelCustomStyle) -> Self {
527        value.0
528    }
529}
530impl From<Style> for LabelCustomStyle {
531    fn from(value: Style) -> Self {
532        Self(value)
533    }
534}
535impl CustomStyle for LabelCustomStyle {
536    type StyleClass = LabelClass;
537}
538
539impl CustomStylable<LabelCustomStyle> for Label {
540    type DV = Self;
541}
542
543impl LabelCustomStyle {
544    pub fn new() -> Self {
545        Self(Style::new())
546    }
547
548    pub fn selectable(mut self, selectable: impl Into<bool>) -> Self {
549        self = Self(self.0.set(Selectable, selectable));
550        self
551    }
552
553    pub fn selection_corner_radius(mut self, corner_radius: impl Into<f64>) -> Self {
554        self = Self(self.0.set(SelectionCornerRadius, corner_radius));
555        self
556    }
557
558    pub fn selection_color(mut self, color: impl Into<Brush>) -> Self {
559        self = Self(self.0.set(CursorColor, color));
560        self
561    }
562}
563impl Default for LabelCustomStyle {
564    fn default() -> Self {
565        Self::new()
566    }
567}