use crate::{ ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine, WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, }; use anyhow::Context as _; use gpui_util::ResultExt; use itertools::Itertools; use smallvec::SmallVec; use std::{ borrow::Cow, cell::{Cell, RefCell}, mem, ops::{Deref, DerefMut, Range}, rc::Rc, sync::Arc, }; /// An [`Element`] that renders text. /// /// In general, [`Text`] objects should be created via the [`text`] macro: /// ```rust /// # use gpui::*; /// # fn render() -> impl IntoElement { /// div().child(text!("hello")) /// # } /// ``` /// ## IDs and Accessibility /// /// [`Text`] elements have an ID. This ID is primarily used to produce nodes in /// the accessibility tree, which allows the text to be visible to screen /// readers and other assistive technologies. /// /// This ID is stable across frames. If the same text, with the same ID, is /// present in two consecutive frames, no updates are reported to the screen /// reader. If the text changes, but the ID stays the same, then the screen /// reader will be notified that a text node's content has changed. **However**, /// if the ID changes, then the screen reader will be notified that a node has /// been removed, and a new node has been added. /// /// When using the [`text`] macro, each invocation of the macro will get a /// unique ID, derived from its position in the source code (filename, line, and /// column). For example: /// ```rust /// # use gpui::*; /// let x = text!("hello"); /// let y = text!("hello"); /// // not equal, because different `text!` invocations produced them /// assert_ne!(x.id(), y.id()); /// /// fn make_text(s: &str) -> Text { text!(s) } /// let x = make_text("hello"); /// let y = make_text("hello"); /// // equal, because the same `text!` invocation produced them /// assert_eq!(x.id(), y.id()); /// ``` /// When the contents of an invocation of [`text`] do not change, this /// distinction is less relevant (with the caveat that you still need to take /// care to ensure that duplicate IDs do not appear). /// /// However, when a [`text`] invocation's argument *does* change, you should /// consider whether this change should be reported as a node "updating its /// contents", or an old node being destroyed and a new node being created. #[derive(Debug, Clone)] pub struct Text { id: Option, text: SharedString, } impl Text { /// Create a new [`Text`] element with a specific ID. /// /// If you want a unique ID to be assigned automatically, use the [`text`] /// macro. The docs for [`Text`] have more detail about choosing IDs. #[inline] pub const fn new(id: ElementId, text: SharedString) -> Self { Self { id: Some(id), text } } /// Create a new [`Text`] element that is inaccessible to screen readers. /// /// In order for text to be accessible to screen readers, it must have an ID /// provided. If you want text to be accessible, either use [`text`] to have /// an ID automatically assigned, or use [`Text::new`] to manually assign an /// ID. /// /// This function is intended for use inside custom UI components, where /// accessible properties may be set on parent containers. #[inline] pub const fn new_inaccessible(text: SharedString) -> Self { Self { id: None, text } } /// The ID of this [`Text`] element. #[inline] pub const fn id(&self) -> Option<&ElementId> { self.id.as_ref() } /// Produce a new [`Text`] with the given `id`. pub fn with_id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } /// The text that this [`Text`] element will display. #[inline] pub const fn text(&self) -> &SharedString { &self.text } } impl Deref for Text { type Target = SharedString; fn deref(&self) -> &Self::Target { &self.text } } impl DerefMut for Text { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.text } } /// Trivial hash function for the location information produced by the [`text`] /// macro. Not covered by semver guarantees. Performance is not particularly /// significant because it's only used on small strings in const contexts. #[doc(hidden)] pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 { const BASIS: u64 = 0xcbf29ce484222325; const PRIME: u64 = 0x100000001b3; let bytes = s.as_bytes(); let mut hash = BASIS; let mut i = 0; while i < bytes.len() { hash ^= bytes[i] as u64; hash = hash.wrapping_mul(PRIME); i += 1; } hash } /// Create a new [`Text`] element. /// /// ```rust /// # use gpui::*; /// let a = text!("hello"); /// let b = text!(id = "farewell-message", "hello"); /// /// ``` /// /// Text created with this macro is *accessible*. The macro generates an ID /// based on the source location. See the docs for [`Text`] for a more in-depth /// explanation of the significance of the ID of a [`Text`] element. #[macro_export] macro_rules! text { (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }}; ($text:expr) => {{ const ID: &'static str = concat!(file!(), "/", line!(), ":", column!()); const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID); $crate::Text::new($crate::ElementId::Integer(HASH), $text.into()) }}; } impl IntoElement for Text { type Element = Self; #[inline] fn into_element(self) -> Self::Element { self } } impl Element for Text { type RequestLayoutState = TextLayout; type PrepaintState = (); fn id(&self) -> Option { self.id.clone() } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn a11y_role(&self) -> Option { if self.id.is_some() { Some(accesskit::Role::Label) } else { None } } fn write_a11y_info(&self, node: &mut accesskit::Node) { node.set_value(self.text.to_string()); } fn request_layout( &mut self, id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { ::request_layout(&mut self.text, id, inspector_id, window, cx) } fn prepaint( &mut self, id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { ::prepaint( &mut self.text, id, inspector_id, bounds, request_layout, window, cx, ) } fn paint( &mut self, id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { ::paint( &mut self.text, id, inspector_id, bounds, request_layout, prepaint, window, cx, ); } } impl Element for &'static str { type RequestLayoutState = TextLayout; type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextLayout::default(); let layout_id = state.layout(SharedString::from(*self), None, window, cx); (layout_id, state) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, text_layout: &mut Self::RequestLayoutState, _window: &mut Window, _cx: &mut App, ) { text_layout.prepaint(bounds, self) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds, text_layout: &mut TextLayout, _: &mut (), window: &mut Window, cx: &mut App, ) { text_layout.paint(self, window, cx) } } impl IntoElement for &'static str { type Element = Self; fn into_element(self) -> Self::Element { self } } impl IntoElement for String { type Element = SharedString; fn into_element(self) -> Self::Element { self.into() } } impl IntoElement for Cow<'static, str> { type Element = SharedString; fn into_element(self) -> Self::Element { self.into() } } impl Element for SharedString { type RequestLayoutState = TextLayout; type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextLayout::default(); let layout_id = state.layout(self.clone(), None, window, cx); (layout_id, state) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, text_layout: &mut Self::RequestLayoutState, _window: &mut Window, _cx: &mut App, ) { text_layout.prepaint(bounds, self.as_ref()) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds, text_layout: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { text_layout.paint(self.as_ref(), window, cx) } } impl IntoElement for SharedString { type Element = Self; fn into_element(self) -> Self::Element { self } } /// Renders text with runs of different styles. /// /// Callers are responsible for setting the correct style for each run. /// For text with a uniform style, you can usually avoid calling this constructor /// and just pass text directly. pub struct StyledText { text: SharedString, runs: Option>, delayed_highlights: Option, HighlightStyle)>>, delayed_font_family_overrides: Option, SharedString)>>, layout: TextLayout, } impl StyledText { /// Construct a new styled text element from the given string. pub fn new(text: impl Into) -> Self { StyledText { text: text.into(), runs: None, delayed_highlights: None, delayed_font_family_overrides: None, layout: TextLayout::default(), } } /// Get the layout for this element. This can be used to map indices to pixels and vice versa. pub fn layout(&self) -> &TextLayout { &self.layout } /// Set the styling attributes for the given text, as well as /// as any ranges of text that have had their style customized. pub fn with_default_highlights( mut self, default_style: &TextStyle, highlights: impl IntoIterator, HighlightStyle)>, ) -> Self { debug_assert!( self.delayed_highlights.is_none(), "Can't use `with_default_highlights` and `with_highlights`" ); let runs = Self::compute_runs(&self.text, default_style, highlights); self.with_runs(runs) } /// Set the styling attributes for the given text, as well as /// as any ranges of text that have had their style customized. pub fn with_highlights( mut self, highlights: impl IntoIterator, HighlightStyle)>, ) -> Self { debug_assert!( self.runs.is_none(), "Can't use `with_highlights` and `with_default_highlights`" ); self.delayed_highlights = Some( highlights .into_iter() .inspect(|(run, _)| { debug_assert!(self.text.is_char_boundary(run.start)); debug_assert!(self.text.is_char_boundary(run.end)); }) .collect::>(), ); self } fn compute_runs( text: &str, default_style: &TextStyle, highlights: impl IntoIterator, HighlightStyle)>, ) -> Vec { let mut runs = Vec::new(); let mut ix = 0; for (range, highlight) in highlights { if ix < range.start { debug_assert!(text.is_char_boundary(range.start)); runs.push(default_style.clone().to_run(range.start - ix)); } debug_assert!(text.is_char_boundary(range.end)); runs.push( default_style .clone() .highlight(highlight) .to_run(range.len()), ); ix = range.end; } if ix < text.len() { runs.push(default_style.to_run(text.len() - ix)); } runs } /// Override the font family for specific byte ranges of the text. /// /// This is resolved lazily at layout time, so the overrides are applied /// on top of the inherited text style from the parent element. /// Can be combined with [`with_highlights`](Self::with_highlights). /// /// The overrides must be sorted by range start and non-overlapping. /// Each override range must fall on character boundaries. pub fn with_font_family_overrides( mut self, overrides: impl IntoIterator, SharedString)>, ) -> Self { self.delayed_font_family_overrides = Some( overrides .into_iter() .inspect(|(range, _)| { debug_assert!(self.text.is_char_boundary(range.start)); debug_assert!(self.text.is_char_boundary(range.end)); }) .collect(), ); self } fn apply_font_family_overrides( runs: &mut [TextRun], overrides: &[(Range, SharedString)], ) { let mut byte_offset = 0; let mut override_idx = 0; for run in runs.iter_mut() { let run_end = byte_offset + run.len; while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset { override_idx += 1; } if override_idx < overrides.len() { let (ref range, ref family) = overrides[override_idx]; if byte_offset >= range.start && run_end <= range.end { run.font.family = family.clone(); } } byte_offset = run_end; } } /// Set the text runs for this piece of text. pub fn with_runs(mut self, runs: Vec) -> Self { let mut text = &*self.text; for run in &runs { text = text.get(run.len..).unwrap_or_else(|| { #[cfg(debug_assertions)] panic!("invalid text run. Text: '{text}', run: {run:?}"); #[cfg(not(debug_assertions))] panic!("invalid text run"); }); } assert!(text.is_empty(), "invalid text run"); self.runs = Some(runs); self } } impl Element for StyledText { type RequestLayoutState = (); type PrepaintState = (); fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let font_family_overrides = self.delayed_font_family_overrides.take(); let mut runs = self.runs.take().or_else(|| { self.delayed_highlights.take().map(|delayed_highlights| { Self::compute_runs(&self.text, &window.text_style(), delayed_highlights) }) }); if let Some(ref overrides) = font_family_overrides { let runs = runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]); Self::apply_font_family_overrides(runs, overrides); } let layout_id = self.layout.layout(self.text.clone(), runs, window, cx); (layout_id, ()) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, _window: &mut Window, _cx: &mut App, ) { self.layout.prepaint(bounds, &self.text) } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, _bounds: Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { self.layout.paint(&self.text, window, cx) } } impl IntoElement for StyledText { type Element = Self; fn into_element(self) -> Self::Element { self } } /// The Layout for TextElement. This can be used to map indices to pixels and vice versa. #[derive(Default, Clone)] pub struct TextLayout(Rc>>); struct TextLayoutInner { len: usize, lines: SmallVec<[WrappedLine; 1]>, line_height: Pixels, wrap_width: Option, size: Option>, bounds: Option>, } /// Metadata about how text should be truncated. Generated during text layout via `TextLayout::evaluate_overflow`. pub struct TextLayoutTruncation { /// The width that the text can occupy before it is truncated. pub width: Option, /// The text to affix to the displayed text if truncating (e.g. an ellipsis `...`). pub affix: SharedString, /// What side of the text will be truncated if it does not fit. pub source: TruncateFrom, } impl TextLayoutTruncation { /// Creates a truncation by using the overflow as the affix, given the provided width. fn overflow_width(text_overflow: TextOverflow, width: Option) -> Self { match text_overflow { TextOverflow::Truncate(s) => TextLayoutTruncation { width, affix: s, source: TruncateFrom::End, }, TextOverflow::TruncateStart(s) => TextLayoutTruncation { width, affix: s, source: TruncateFrom::Start, }, } } } impl TextLayout { /// Evaluates the width to wrap the text at. pub fn evaluate_wrap_width( white_space: &WhiteSpace, known_dimensions: Size>, available_space: Size, ) -> Option { use crate::AvailableSpace::*; match white_space { // Text does not wrap, no max width WhiteSpace::Nowrap => None, // If the text wraps, return the already calculated width. WhiteSpace::Normal => known_dimensions.width.or(match available_space.width { // Otherwise if the available space is a concrete value, then that is the width to wrap to. Definite(x) => Some(x), // If the wrapping is content-based, then there is no wrapping of text. MaxContent | MinContent => None, }), } } /// Evaluates how truncation should be applied if the text overflows the available space. pub fn evaluate_overflow( text_style: &TextStyle, known_dimensions: Size>, available_space: Size, ) -> TextLayoutTruncation { match text_style.text_overflow.clone() { Some(text_overflow) => { // Calculate the desired width, prioritizing the calculated dimensions, // falling back on calculating a width from the available space and // number of lines to clamp to via text style. let width = known_dimensions.width.or(match available_space.width { crate::AvailableSpace::Definite(x) => match text_style.line_clamp { Some(max_lines) => Some(x * max_lines), None => Some(x), }, _ => None, }); TextLayoutTruncation::overflow_width(text_overflow, width) } None => TextLayoutTruncation { width: None, affix: SharedString::default(), source: TruncateFrom::End, }, } } /// Conditionally applies truncation to some text and outputs how the text should be displayed. pub fn apply_truncation<'runs>( text: SharedString, text_style: &TextStyle, font_size: Pixels, wrap_width: Option, truncation: &TextLayoutTruncation, runs: &'runs [TextRun], cx: &mut App, ) -> (SharedString, Cow<'runs, [TextRun]>) { let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); if truncation.width.is_some() { if let Some(max_lines) = text_style.line_clamp && let Some(wrap_width) = wrap_width { line_wrapper.truncate_wrapped_line( text, wrap_width, max_lines, &truncation.affix, &runs, truncation.source, ) } else { line_wrapper.truncate_line( text, truncation.width.unwrap_or(Pixels::MAX), &truncation.affix, &runs, truncation.source, ) } } else { (text, std::borrow::Cow::Borrowed(runs)) } } fn layout( &self, text: SharedString, runs: Option>, window: &mut Window, _: &mut App, ) -> LayoutId { let text_style = window.text_style(); let font_size = text_style.font_size.to_pixels(window.rem_size()); let line_height = window.pixel_snap( text_style .line_height .to_pixels(font_size.into(), window.rem_size()), ); let runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] }; window.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space, window, cx| { let wrap_width = Self::evaluate_wrap_width( &text_style.white_space, known_dimensions, available_space, ); let truncation = Self::evaluate_overflow(&text_style, known_dimensions, available_space); // Only use cached layout if: // 1. We have a cached size // 2. wrap_width matches (or both are None) // 3. truncate_width is None (if truncate_width is Some, we need to re-layout // because the previous layout may have been computed without truncation) if let Some(text_layout) = element_state.0.borrow().as_ref() && let Some(size) = text_layout.size && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) && truncation.width.is_none() { return size; } let (text, runs) = Self::apply_truncation( text.clone(), &text_style, font_size, wrap_width, &truncation, &runs, cx, ); let len = text.len(); let Some(lines) = window .text_system() .shape_text( text, font_size, &runs, wrap_width, // Wrap if we know the width. text_style.line_clamp, // Limit the number of lines if line_clamp is set. ) .log_err() else { element_state.0.borrow_mut().replace(TextLayoutInner { lines: Default::default(), len: 0, line_height, wrap_width, size: Some(Size::default()), bounds: None, }); return Size::default(); }; let mut size: Size = Size::default(); for line in &lines { let line_size = line.size(line_height); size.height += line_size.height; size.width = size.width.max(line_size.width).ceil(); } element_state.0.borrow_mut().replace(TextLayoutInner { lines, len, line_height, wrap_width, size: Some(size), bounds: None, }); size } }) } fn prepaint(&self, bounds: Bounds, text: &str) { let mut element_state = self.0.borrow_mut(); let element_state = element_state .as_mut() .with_context(|| format!("measurement has not been performed on {text}")) .unwrap(); element_state.bounds = Some(bounds); } fn paint(&self, text: &str, window: &mut Window, cx: &mut App) { let element_state = self.0.borrow(); let element_state = element_state .as_ref() .with_context(|| format!("measurement has not been performed on {text}")) .unwrap(); let bounds = element_state .bounds .with_context(|| format!("prepaint has not been performed on {text}")) .unwrap(); let line_height = element_state.line_height; let mut line_origin = bounds.origin; let text_style = window.text_style(); for line in &element_state.lines { line.paint_background( line_origin, line_height, text_style.text_align, Some(bounds), window, cx, ) .log_err(); line.paint( line_origin, line_height, text_style.text_align, Some(bounds), window, cx, ) .log_err(); line_origin.y += line.size(line_height).height; } } /// Get the byte index into the input of the pixel position. pub fn index_for_position(&self, mut position: Point) -> Result { let element_state = self.0.borrow(); let element_state = element_state .as_ref() .expect("measurement has not been performed"); let bounds = element_state .bounds .expect("prepaint has not been performed"); if position.y < bounds.top() { return Err(0); } let line_height = element_state.line_height; let mut line_origin = bounds.origin; let mut line_start_ix = 0; for line in &element_state.lines { let line_bottom = line_origin.y + line.size(line_height).height; if position.y > line_bottom { line_origin.y = line_bottom; line_start_ix += line.len() + 1; } else { let position_within_line = position - line_origin; match line.index_for_position(position_within_line, line_height) { Ok(index_within_line) => return Ok(line_start_ix + index_within_line), Err(index_within_line) => return Err(line_start_ix + index_within_line), } } } Err(line_start_ix.saturating_sub(1)) } /// Get the pixel position for the given byte index. pub fn position_for_index(&self, index: usize) -> Option> { let element_state = self.0.borrow(); let element_state = element_state .as_ref() .expect("measurement has not been performed"); let bounds = element_state .bounds .expect("prepaint has not been performed"); let line_height = element_state.line_height; let mut line_origin = bounds.origin; let mut line_start_ix = 0; for line in &element_state.lines { let line_end_ix = line_start_ix + line.len(); if index < line_start_ix { break; } else if index > line_end_ix { line_origin.y += line.size(line_height).height; line_start_ix = line_end_ix + 1; continue; } else { let ix_within_line = index - line_start_ix; return Some(line_origin + line.position_for_index(ix_within_line, line_height)?); } } None } /// Retrieve the layout for the line containing the given byte index. pub fn line_layout_for_index(&self, index: usize) -> Option> { let element_state = self.0.borrow(); let element_state = element_state .as_ref() .expect("measurement has not been performed"); let bounds = element_state .bounds .expect("prepaint has not been performed"); let line_height = element_state.line_height; let mut line_origin = bounds.origin; let mut line_start_ix = 0; for line in &element_state.lines { let line_end_ix = line_start_ix + line.len(); if index < line_start_ix { break; } else if index > line_end_ix { line_origin.y += line.size(line_height).height; line_start_ix = line_end_ix + 1; continue; } else { return Some(line.layout.clone()); } } None } /// The bounds of this layout. pub fn bounds(&self) -> Bounds { self.0.borrow().as_ref().unwrap().bounds.unwrap() } /// The line height for this layout. pub fn line_height(&self) -> Pixels { self.0.borrow().as_ref().unwrap().line_height } /// The UTF-8 length of the underlying text. pub fn len(&self) -> usize { self.0.borrow().as_ref().unwrap().len } /// The text for this layout. pub fn text(&self) -> String { self.0 .borrow() .as_ref() .unwrap() .lines .iter() .map(|s| &s.text) .join("\n") } /// The text for this layout (with soft-wraps as newlines) pub fn wrapped_text(&self) -> String { let mut accumulator = String::new(); for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() { let mut seen = 0; for boundary in wrapped.layout.wrap_boundaries.iter() { let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs [boundary.glyph_ix] .index; accumulator.push_str(&wrapped.text[seen..index]); accumulator.push('\n'); seen = index; } accumulator.push_str(&wrapped.text[seen..]); accumulator.push('\n'); } // Remove trailing newline accumulator.pop(); accumulator } } /// A text element that can be interacted with. pub struct InteractiveText { element_id: ElementId, text: StyledText, click_listener: Option], InteractiveTextClickEvent, &mut Window, &mut App)>>, hover_listener: Option, MouseMoveEvent, &mut Window, &mut App)>>, tooltip_builder: Option Option>>, tooltip_id: Option, clickable_ranges: Vec>, } struct InteractiveTextClickEvent { mouse_down_index: usize, mouse_up_index: usize, } #[doc(hidden)] #[derive(Default)] pub struct InteractiveTextState { mouse_down_index: Rc>>, hovered_index: Rc>>, active_tooltip: Rc>>, } /// InteractiveTest is a wrapper around StyledText that adds mouse interactions. impl InteractiveText { /// Creates a new InteractiveText from the given text. pub fn new(id: impl Into, text: StyledText) -> Self { Self { element_id: id.into(), text, click_listener: None, hover_listener: None, tooltip_builder: None, tooltip_id: None, clickable_ranges: Vec::new(), } } /// on_click is called when the user clicks on one of the given ranges, passing the index of /// the clicked range. pub fn on_click( mut self, ranges: Vec>, listener: impl Fn(usize, &mut Window, &mut App) + 'static, ) -> Self { self.click_listener = Some(Box::new(move |ranges, event, window, cx| { for (range_ix, range) in ranges.iter().enumerate() { if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) { listener(range_ix, window, cx); } } })); self.clickable_ranges = ranges; self } /// on_hover is called when the mouse moves over a character within the text, passing the /// index of the hovered character, or None if the mouse leaves the text. pub fn on_hover( mut self, listener: impl Fn(Option, MouseMoveEvent, &mut Window, &mut App) + 'static, ) -> Self { self.hover_listener = Some(Box::new(listener)); self } /// tooltip lets you specify a tooltip for a given character index in the string. pub fn tooltip( mut self, builder: impl Fn(usize, &mut Window, &mut App) -> Option + 'static, ) -> Self { self.tooltip_builder = Some(Rc::new(builder)); self } } impl Element for InteractiveText { type RequestLayoutState = (); type PrepaintState = Hitbox; fn id(&self) -> Option { Some(self.element_id.clone()) } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { None } fn a11y_role(&self) -> Option { Some(accesskit::Role::Label) } fn write_a11y_info(&self, node: &mut accesskit::Node) { node.set_value(self.text.text.to_string()); } fn request_layout( &mut self, _id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { self.text.request_layout(None, inspector_id, window, cx) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, state: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Hitbox { window.with_optional_element_state::( global_id, |interactive_state, window| { let mut interactive_state = interactive_state .map(|interactive_state| interactive_state.unwrap_or_default()); if let Some(interactive_state) = interactive_state.as_mut() { if self.tooltip_builder.is_some() { self.tooltip_id = set_tooltip_on_window(&interactive_state.active_tooltip, window); } else { // If there is no longer a tooltip builder, remove the active tooltip. interactive_state.active_tooltip.take(); } } self.text .prepaint(None, inspector_id, bounds, state, window, cx); let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); (hitbox, interactive_state) }, ) } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, hitbox: &mut Hitbox, window: &mut Window, cx: &mut App, ) { let current_view = window.current_view(); let text_layout = self.text.layout().clone(); window.with_element_state::( global_id.unwrap(), |interactive_state, window| { let mut interactive_state = interactive_state.unwrap_or_default(); if let Some(click_listener) = self.click_listener.take() { let mouse_position = window.mouse_position(); if let Ok(ix) = text_layout.index_for_position(mouse_position) && self .clickable_ranges .iter() .any(|range| range.contains(&ix)) { window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) } let text_layout = text_layout.clone(); let mouse_down = interactive_state.mouse_down_index.clone(); if let Some(mouse_down_index) = mouse_down.get() { let hitbox = hitbox.clone(); let clickable_ranges = mem::take(&mut self.clickable_ranges); window.on_mouse_event( move |event: &MouseUpEvent, phase, window: &mut Window, cx| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { if let Ok(mouse_up_index) = text_layout.index_for_position(event.position) { click_listener( &clickable_ranges, InteractiveTextClickEvent { mouse_down_index, mouse_up_index, }, window, cx, ) } mouse_down.take(); window.refresh(); } }, ); } else { let hitbox = hitbox.clone(); window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) && let Ok(mouse_down_index) = text_layout.index_for_position(event.position) { mouse_down.set(Some(mouse_down_index)); window.refresh(); } }); } } window.on_mouse_event({ let mut hover_listener = self.hover_listener.take(); let hitbox = hitbox.clone(); let text_layout = text_layout.clone(); let hovered_index = interactive_state.hovered_index.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { let current = hovered_index.get(); let updated = text_layout.index_for_position(event.position).ok(); if current != updated { hovered_index.set(updated); if let Some(hover_listener) = hover_listener.as_ref() { hover_listener(updated, event.clone(), window, cx); } cx.notify(current_view); } } } }); if let Some(tooltip_builder) = self.tooltip_builder.clone() { let active_tooltip = interactive_state.active_tooltip.clone(); let build_tooltip = Rc::new({ let tooltip_is_hoverable = false; let text_layout = text_layout.clone(); move |window: &mut Window, cx: &mut App| { text_layout .index_for_position(window.mouse_position()) .ok() .and_then(|position| tooltip_builder(position, window, cx)) .map(|view| (view, tooltip_is_hoverable)) } }); // Use bounds instead of testing hitbox since this is called during prepaint. let check_is_hovered_during_prepaint = Rc::new({ let source_bounds = hitbox.bounds; let text_layout = text_layout.clone(); let pending_mouse_down = interactive_state.mouse_down_index.clone(); move |window: &Window| { text_layout .index_for_position(window.mouse_position()) .is_ok() && source_bounds.contains(&window.mouse_position()) && pending_mouse_down.get().is_none() } }); let check_is_hovered = Rc::new({ let hitbox = hitbox.clone(); let text_layout = text_layout.clone(); let pending_mouse_down = interactive_state.mouse_down_index.clone(); move |window: &Window| { text_layout .index_for_position(window.mouse_position()) .is_ok() && hitbox.is_hovered(window) && pending_mouse_down.get().is_none() } }); register_tooltip_mouse_handlers( &active_tooltip, self.tooltip_id, build_tooltip, check_is_hovered, check_is_hovered_during_prepaint, window, ); } self.text .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); ((), interactive_state) }, ); } } impl IntoElement for InteractiveText { type Element = Self; fn into_element(self) -> Self::Element { self } } #[cfg(test)] mod tests { use super::*; #[test] fn test_into_element_for() { use crate::{ParentElement as _, SharedString, div}; use std::borrow::Cow; let _ = div().child("static str"); let _ = div().child("String".to_string()); let _ = div().child(Cow::Borrowed("Cow")); let _ = div().child(SharedString::from("SharedString")); } #[test] fn text_macro_id() { // one call to `text!` = one id fn make_text_stable_id(happy: bool) -> Text { text!(if happy { "happy" } else { "sad" }) } // two calls to `text!` = two ids fn make_text_unstable_id(happy: bool) -> Text { if happy { text!("happy") } else { text!("sad") } } assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id); assert_ne!( make_text_unstable_id(false).id, make_text_unstable_id(true).id ); } }