Skip to main content

floem/views/
label.rs

1use std::{any::Any, cell::RefCell, fmt::Display, rc::Rc};
2
3use crate::{
4    Clipboard, ViewId,
5    context::{EventCx, LayoutChangedListener, PaintCx, UpdateCx},
6    event::{Event, EventPropagation, FocusEvent, Phase, listener},
7    prelude::EventListenerTrait,
8    prop_extractor,
9    style::{
10        ContextValue, CustomStylable, CustomStyle, ExprStyle, FontProps, LineHeight, Selectable,
11        SelectionCornerRadius, SelectionStyle, Style, TextAlignProp, TextColor, TextOverflow,
12        TextOverflowProp,
13    },
14    style_class,
15    text::{
16        Attrs, AttrsList, Cursor, FamilyOwned, TextLayout, TextLayoutState, TextSelection,
17        WordBreakStrength,
18    },
19    view::{LayoutNodeCx, View},
20    views::editor::SelectionColor,
21};
22use floem_reactive::UpdaterEffect;
23use floem_renderer::Renderer;
24use peniko::{
25    Brush,
26    color::palette::{self},
27    kurbo::Point,
28};
29use ui_events::{
30    keyboard::{Key, KeyState, KeyboardEvent},
31    pointer::{PointerButtonEvent, PointerEvent},
32};
33
34use super::{Decorators, TextCommand};
35
36prop_extractor! {
37    LabelProps {
38        color: TextColor,
39        text_overflow: TextOverflowProp,
40        line_height: LineHeight,
41        text_selectable: Selectable,
42        text_align: TextAlignProp,
43    }
44}
45
46style_class!(
47    /// The style class that is applied to labels.
48    pub LabelClass
49);
50
51/// Event fired when a text view's overflow state changes.
52///
53/// This is fired when text transitions between fitting within its bounds and overflowing,
54/// or vice versa. The overflow state depends on the `text_overflow` style property:
55///
56/// - `TextOverflow::NoWrap(NoWrapOverflow::Clip)`: Text is clipped at the boundary
57/// - `TextOverflow::NoWrap(NoWrapOverflow::Ellipsis)`: Text is truncated with "..." when it overflows
58/// - `TextOverflow::Wrap { .. }`: Text wraps to multiple lines (changes line count, not overflow state)
59///
60/// # Use Cases
61///
62/// - Show/hide a "more" button when text is truncated
63/// - Toggle between single-line and multi-line display modes
64/// - Display tooltips with full text when content is clipped
65/// - Update UI indicators when text fits or overflows
66///
67/// # Example
68///
69/// ```rust
70/// # use floem::event::EventPropagation;
71/// # use floem::prelude::*;
72/// # use floem::style::{NoWrapOverflow, TextOverflow};
73/// # use floem::text::TextOverflowChanged;
74/// Label::derived(|| "Some long text that might overflow")
75///     .style(|s| s.text_overflow(TextOverflow::NoWrap(NoWrapOverflow::Ellipsis)))
76///     .on_event(TextOverflowChanged::listener(), |_cx, event| {
77///         if event.is_overflowing {
78///             println!("Text is now overflowing and truncated");
79///         } else {
80///             println!("Text fits completely");
81///         }
82///         EventPropagation::Continue
83///     });
84/// ```
85#[derive(Debug, Clone, PartialEq)]
86enum SelectionState {
87    None,
88    Ready {
89        origin: Point,
90        selection: TextSelection,
91    },
92    Selecting(TextSelection),
93    Selected(TextSelection),
94}
95
96/// A View that can display text from a [`String`]. See [`label`], [`text`], and [`static_label`].
97pub struct Label {
98    id: ViewId,
99    label: String,
100    /// Layout data containing text layouts and overflow handling logic
101    layout_data: Rc<RefCell<TextLayoutState>>,
102    selection_state: SelectionState,
103    selection_style: SelectionStyle,
104    font_props: FontProps,
105    label_props: LabelProps,
106    text_node: Option<taffy::NodeId>,
107    layout_node: Option<taffy::NodeId>,
108}
109
110impl Label {
111    fn new_internal(id: ViewId, label: String) -> Self {
112        id.register_listener(LayoutChangedListener::listener_key());
113        let layout_data = Rc::new(RefCell::new(TextLayoutState::new(Some(id))));
114        let mut label = Label {
115            id,
116            label,
117            layout_data,
118            text_node: None,
119            layout_node: None,
120            selection_state: SelectionState::None,
121            selection_style: Default::default(),
122            font_props: FontProps::default(),
123            label_props: Default::default(),
124        };
125        label.set_text_layout();
126        label.set_taffy_layout();
127        label.class(LabelClass)
128    }
129
130    /// Creates a new non-reactive label from any type that implements [`Display`].
131    ///
132    /// ## Example
133    /// ```rust
134    /// use floem::views::*;
135    ///
136    /// Label::new("Hello, world!");
137    /// Label::new(42);
138    /// ```
139    pub fn new<S: Display>(label: S) -> Self {
140        Self::new_internal(ViewId::new(), label.to_string())
141    }
142
143    /// Creates a new non-reactive label with a pre-existing [`ViewId`].
144    ///
145    /// This is useful for lazy view construction where the `ViewId` is created
146    /// before the view itself.
147    pub fn with_id<S: Display>(id: ViewId, label: S) -> Self {
148        Self::new_internal(id, label.to_string())
149    }
150
151    /// Creates a derived label that automatically updates when its dependencies change.
152    ///
153    /// ## Example
154    /// ```rust
155    /// use floem::{reactive::*, views::*};
156    ///
157    /// let count = RwSignal::new(0);
158    /// Label::derived(move || format!("Count: {}", count.get()));
159    /// ```
160    pub fn derived<S: Display + 'static>(label: impl Fn() -> S + 'static) -> Self {
161        let id = ViewId::new();
162        let initial_label = UpdaterEffect::new(
163            move || label().to_string(),
164            move |new_label| id.update_state(new_label),
165        );
166        Self::new_internal(id, initial_label).on_event_cont(listener::FocusLost, move |_, _| {
167            id.request_layout();
168        })
169    }
170
171    fn with_effective_text_layout<O>(&self, with: impl FnOnce(&TextLayout) -> O) -> O {
172        self.layout_data.borrow().with_effective_text_layout(with)
173    }
174}
175
176/// A non-reactive view that can display text from an item that implements [`Display`]. See also [`label`].
177///
178/// ## Example
179/// ```rust
180/// use floem::views::*;
181///
182/// stack((
183///    text("non-reactive-text"),
184///    text(505),
185/// ));
186/// ```
187#[deprecated(since = "0.2.0", note = "Use Label::new() instead")]
188pub fn text<S: Display>(text: S) -> Label {
189    Label::new(text)
190}
191
192/// A non-reactive view that can display text from an item that can be turned into a [`String`]. See also [`label`].
193#[deprecated(since = "0.2.0", note = "Use Label::new() instead")]
194pub fn static_label(label: impl Into<String>) -> Label {
195    Label::new(label.into())
196}
197
198/// A view that can reactively display text from an item that implements [`Display`]. See also [`text`] for a non-reactive label.
199///
200/// ## Example
201/// ```rust
202/// use floem::{reactive::*, views::*};
203///
204/// let text = RwSignal::new("Reactive text to be displayed".to_string());
205///
206/// label(move || text.get());
207/// ```
208#[deprecated(since = "0.2.0", note = "Use Label::derived() instead")]
209pub fn label<S: Display + 'static>(label: impl Fn() -> S + 'static) -> Label {
210    Label::derived(label)
211}
212
213impl Label {
214    fn mark_text_measure_dirty(&self) {
215        if let Some(text_node) = self.text_node {
216            let _ = self.id.taffy().borrow_mut().mark_dirty(text_node);
217        }
218        let _ = self.id.mark_view_layout_dirty();
219    }
220
221    fn get_attrs_list(&self) -> AttrsList {
222        let mut attrs = Attrs::new().color(self.label_props.color().unwrap_or(palette::css::BLACK));
223        let font_size = self.font_props.size();
224        attrs = attrs.font_size(font_size as f32);
225
226        if let Some(font_style) = self.font_props.style() {
227            attrs = attrs.font_style(font_style);
228        }
229        let font_family = self.font_props.family().as_ref().map(|font_family| {
230            let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
231            family
232        });
233        if let Some(font_family) = font_family.as_ref() {
234            attrs = attrs.family(font_family);
235        }
236        if let Some(font_weight) = self.font_props.weight() {
237            attrs = attrs.weight(font_weight);
238        }
239        attrs = attrs.line_height(self.label_props.line_height());
240        if let TextOverflow::Wrap { word_break, .. } = self.label_props.text_overflow()
241            && word_break != WordBreakStrength::Normal
242        {
243            attrs = attrs.word_break(word_break);
244        }
245        AttrsList::new(attrs)
246    }
247
248    fn set_text_layout(&mut self) {
249        let attrs_list = self.get_attrs_list();
250        let align = self.label_props.text_align();
251        let text_overflow = self.label_props.text_overflow();
252
253        let mut layout_data = self.layout_data.borrow_mut();
254        layout_data.set_text(&self.label, attrs_list, align);
255        layout_data.set_text_overflow(text_overflow);
256
257        self.mark_text_measure_dirty();
258    }
259
260    fn get_hit_point(&self, point: Point) -> Option<Cursor> {
261        let (Some(parent_node), Some(text_node)) = (self.layout_node, self.text_node) else {
262            return None;
263        };
264
265        let text_loc = self.get_text_origin(parent_node, text_node);
266        self.with_effective_text_layout(|l| l.hit_test(point - text_loc.to_vec2()))
267    }
268
269    fn get_text_origin(&self, parent_node: taffy::NodeId, text_node: taffy::NodeId) -> Point {
270        let content_rect = self
271            .id
272            .get_content_rect_relative(text_node, parent_node)
273            .unwrap_or_default();
274        self.layout_data.borrow().centered_text_origin(content_rect)
275    }
276
277    fn update_drag_selection(&mut self, focus_point: Point) {
278        let Some(focus) = self.get_hit_point(focus_point) else {
279            return;
280        };
281
282        match self.selection_state {
283            SelectionState::Ready { selection, .. } => {
284                let next_selection = self
285                    .layout_data
286                    .borrow()
287                    .get_effective_text_layout()
288                    .map(|layout| layout.begin_selection(selection.anchor(), focus))
289                    .expect("label text layout should be available while selecting");
290                self.selection_state = SelectionState::Selecting(next_selection);
291            }
292            SelectionState::Selecting(selection) | SelectionState::Selected(selection) => {
293                let selection = self
294                    .layout_data
295                    .borrow()
296                    .get_effective_text_layout()
297                    .map(|layout| layout.selection(selection.anchor(), focus))
298                    .expect("label text layout should be available while selecting");
299                self.selection_state = SelectionState::Selecting(selection);
300            }
301            SelectionState::None => {}
302        }
303    }
304
305    fn selection(&self) -> Option<TextSelection> {
306        match self.selection_state {
307            SelectionState::Selecting(selection) | SelectionState::Selected(selection)
308                if !selection.is_collapsed() =>
309            {
310                Some(selection)
311            }
312            SelectionState::Ready { .. } | SelectionState::None => None,
313            SelectionState::Selecting(_) | SelectionState::Selected(_) => None,
314        }
315    }
316
317    fn handle_modifier_cmd(&mut self, command: &TextCommand) -> bool {
318        match command {
319            TextCommand::Copy => {
320                if let Some(selection) = self.selection() {
321                    let layout_data = self.layout_data.borrow();
322                    if let Some(text_layout) = layout_data.get_effective_text_layout() {
323                        let range = text_layout.selection_text_range(&selection);
324                        let selection_txt = self.label[range].into();
325                        let _ = Clipboard::set_contents(selection_txt);
326                    }
327                }
328                true
329            }
330            _ => false,
331        }
332    }
333    fn handle_key_down(&mut self, event: &KeyboardEvent) -> bool {
334        if event.modifiers.is_empty() {
335            return false;
336        }
337        if !matches!(event.key, Key::Character(_)) {
338            return false;
339        }
340
341        self.handle_modifier_cmd(&event.into())
342    }
343
344    fn paint_selection(&self, text_loc: Point, paint_cx: &mut PaintCx) {
345        if let Some(selection) = self.selection() {
346            let selection_color = self.selection_style.selection_color();
347            self.layout_data
348                .borrow()
349                .selection_rects_for_selection(&selection, text_loc, |rect| {
350                    paint_cx.fill(&rect, &selection_color, 0.0);
351                });
352        }
353    }
354
355    fn set_taffy_layout(&mut self) {
356        let taffy_node = self.id.taffy_node();
357        let taffy = self.id.taffy();
358        let mut taffy = taffy.borrow_mut();
359        let text_node = taffy
360            .new_leaf(taffy::Style {
361                ..taffy::Style::DEFAULT
362            })
363            .unwrap();
364
365        let layout_fn = TextLayoutState::create_taffy_layout_fn(self.layout_data.clone());
366        let finalize_fn = TextLayoutState::create_finalize_fn(self.layout_data.clone());
367        self.text_node = Some(text_node);
368        self.layout_node = Some(taffy_node);
369
370        taffy
371            .set_node_context(
372                text_node,
373                Some(LayoutNodeCx::Custom {
374                    measure: layout_fn,
375                    finalize: Some(finalize_fn),
376                }),
377            )
378            .unwrap();
379        taffy.set_children(taffy_node, &[text_node]).unwrap();
380    }
381}
382
383impl View for Label {
384    fn id(&self) -> ViewId {
385        self.id
386    }
387
388    fn view_style(&self) -> Option<Style> {
389        None
390    }
391
392    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
393        format!("Label: {:?}", self.label).into()
394    }
395
396    fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
397        if let Some(layout) = LayoutChangedListener::extract(&cx.event) {
398            self.layout_data
399                .borrow_mut()
400                .finalize_for_width(layout.new_content_box.width() as f32);
401        }
402        match &cx.event {
403            Event::Focus(FocusEvent::Lost) => {
404                self.selection_state = SelectionState::None;
405                cx.window_state.request_paint(self.id);
406                return EventPropagation::Continue;
407            }
408            Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. }))
409                if self.label_props.text_selectable()
410                    && state
411                        .buttons
412                        .contains(ui_events::pointer::PointerButton::Primary) =>
413            {
414                self.selection_state = self
415                    .get_hit_point(state.logical_point())
416                    .map(|cursor| SelectionState::Ready {
417                        origin: state.logical_point(),
418                        selection: self
419                            .layout_data
420                            .borrow()
421                            .get_effective_text_layout()
422                            .map(|layout| layout.collapsed_selection(cursor))
423                            .expect("label text layout should be available on pointer down"),
424                    })
425                    .unwrap_or(SelectionState::None);
426                if let Some(pointer_id) = pointer.pointer_id {
427                    cx.window_state.set_pointer_capture(pointer_id, self.id);
428                }
429                cx.window_state.request_paint(self.id);
430            }
431            Event::Pointer(PointerEvent::Move(pu)) => {
432                if !self.label_props.text_selectable() {
433                    if self.selection_state != SelectionState::None {
434                        self.selection_state = SelectionState::None;
435                        cx.window_state.request_paint(self.id);
436                    }
437                } else {
438                    match self.selection_state {
439                        SelectionState::Ready { origin, .. } => {
440                            // This check prevents text selection from eating tiny pointer moves.
441                            if origin.distance(pu.current.logical_point()).abs() > 2. {
442                                self.update_drag_selection(pu.current.logical_point());
443                                cx.window_state.request_paint(self.id);
444                                self.id.request_focus();
445                            }
446                        }
447                        SelectionState::Selecting(_) => {
448                            self.update_drag_selection(pu.current.logical_point());
449                            cx.window_state.request_paint(self.id);
450                        }
451                        SelectionState::Selected(_) => return EventPropagation::Continue,
452                        SelectionState::None => return EventPropagation::Continue,
453                    }
454                }
455            }
456            Event::Pointer(PointerEvent::Up { .. }) => {
457                self.selection_state = match self.selection_state {
458                    SelectionState::Selecting(selection) if !selection.is_collapsed() => {
459                        SelectionState::Selected(selection)
460                    }
461                    _ => SelectionState::None,
462                };
463                cx.window_state.request_paint(self.id);
464            }
465            Event::Key(
466                ke @ KeyboardEvent {
467                    state: KeyState::Down,
468                    ..
469                },
470            ) => {
471                if cx.phase != Phase::Target || !cx.window_state.is_focused(self.id) {
472                    return EventPropagation::Continue;
473                }
474                if self.handle_key_down(ke) {
475                    return EventPropagation::Stop;
476                }
477            }
478            _ => {}
479        }
480        EventPropagation::Continue
481    }
482
483    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
484        if self.font_props.read(cx) | self.label_props.read(cx) {
485            self.layout_data.borrow_mut().clear_overflow_state();
486            self.set_text_layout();
487            self.id.request_layout();
488            self.id.request_paint();
489        }
490        if self.selection_style.read(cx) {
491            cx.window_state.request_paint(self.id);
492        }
493    }
494
495    fn update(&mut self, cx: &mut UpdateCx, state: Box<dyn Any>) {
496        if state.is::<String>()
497            && let Ok(state) = state.downcast::<String>()
498        {
499            self.label = *state;
500            self.layout_data.borrow_mut().clear_overflow_state();
501            self.set_text_layout();
502            cx.window_state.schedule_layout();
503        }
504    }
505
506    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
507        if self.label.is_empty() {
508            return;
509        }
510
511        let (Some(this_node), Some(text_node)) = (self.layout_node, self.text_node) else {
512            return;
513        };
514
515        let text_loc = self.get_text_origin(this_node, text_node);
516
517        self.with_effective_text_layout(|l| {
518            l.draw(cx, text_loc);
519            if cx.window_state.is_focused(self.id) {
520                self.paint_selection(text_loc, cx);
521            }
522        });
523    }
524}
525
526/// Represents a custom style for a `Label`.
527#[derive(Debug, Clone)]
528pub struct LabelCustomStyle(Style);
529impl From<LabelCustomStyle> for Style {
530    fn from(value: LabelCustomStyle) -> Self {
531        value.0
532    }
533}
534impl From<Style> for LabelCustomStyle {
535    fn from(value: Style) -> Self {
536        Self(value)
537    }
538}
539impl CustomStyle for LabelCustomStyle {
540    type StyleClass = LabelClass;
541}
542
543impl CustomStylable<LabelCustomStyle> for Label {
544    type DV = Self;
545}
546
547impl LabelCustomStyle {
548    pub fn new() -> Self {
549        Self(Style::new())
550    }
551
552    pub fn selectable(mut self, selectable: impl Into<bool>) -> Self {
553        self = Self(self.0.set(Selectable, selectable));
554        self
555    }
556
557    pub fn selection_corner_radius(mut self, corner_radius: impl Into<f64>) -> Self {
558        self = Self(self.0.set(SelectionCornerRadius, corner_radius));
559        self
560    }
561
562    pub fn selection_color(mut self, color: impl Into<Brush>) -> Self {
563        self = Self(self.0.set(SelectionColor, color.into()));
564        self
565    }
566}
567impl Default for LabelCustomStyle {
568    fn default() -> Self {
569        Self::new()
570    }
571}
572
573#[derive(Clone, Default)]
574pub struct LabelCustomExprStyle(Style);
575impl From<LabelCustomExprStyle> for Style {
576    fn from(value: LabelCustomExprStyle) -> Self {
577        value.0
578    }
579}
580impl From<Style> for LabelCustomExprStyle {
581    fn from(value: Style) -> Self {
582        Self(value)
583    }
584}
585impl LabelCustomExprStyle {
586    pub fn new() -> Self {
587        Self(Style::new())
588    }
589
590    pub fn selectable<T>(mut self, selectable: ContextValue<T>) -> Self
591    where
592        T: Into<bool> + 'static,
593    {
594        self = Self(
595            ExprStyle::from(self.0)
596                .set_context(Selectable, selectable.map(Into::into))
597                .into(),
598        );
599        self
600    }
601
602    pub fn selection_corner_radius<T>(mut self, corner_radius: ContextValue<T>) -> Self
603    where
604        T: Into<f64> + 'static,
605    {
606        self = Self(
607            ExprStyle::from(self.0)
608                .set_context(SelectionCornerRadius, corner_radius.map(Into::into))
609                .into(),
610        );
611        self
612    }
613
614    pub fn selection_color<T>(mut self, color: ContextValue<T>) -> Self
615    where
616        T: Into<Brush> + 'static,
617    {
618        self = Self(
619            ExprStyle::from(self.0)
620                .set_context(SelectionColor, color.map(Into::into))
621                .into(),
622        );
623        self
624    }
625}