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
55pub 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
99pub fn text<S: Display>(text: S) -> Label {
111 static_label(text.to_string())
112}
113
114pub fn static_label(label: impl Into<String>) -> Label {
116 Label::new(ViewId::new(), label.into())
117}
118
119pub 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 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 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#[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}