Skip to main content

floem/views/
label.rs

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