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 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
62pub 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 pub fn new<S: Display>(label: S) -> Self {
109 Self::new_internal(ViewId::new(), label.to_string())
110 }
111
112 pub fn with_id<S: Display>(id: ViewId, label: S) -> Self {
117 Self::new_internal(id, label.to_string())
118 }
119
120 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#[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#[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#[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 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 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#[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}