use crate::{ ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, }; use anyhow::Context as _; use smallvec::SmallVec; use std::{ cell::{Cell, RefCell}, mem, ops::Range, rc::Rc, sync::Arc, }; use util::ResultExt; impl Element for &'static str { type RequestLayoutState = TextLayout; type PrepaintState = (); fn id(&self) -> Option { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, 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>, 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>, _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 Element for SharedString { type RequestLayoutState = TextLayout; type PrepaintState = (); fn id(&self) -> Option { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, 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>, 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>, _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)>>, 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, 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.runs = Some(runs); self } /// 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().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 { runs.push(default_style.clone().to_run(range.start - ix)); } 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 } /// Set the text runs for this piece of text. pub fn with_runs(mut self, runs: Vec) -> Self { self.runs = Some(runs); self } } impl Element for StyledText { type RequestLayoutState = (); type PrepaintState = (); fn id(&self) -> Option { None } fn request_layout( &mut self, _id: Option<&GlobalElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let runs = self.runs.take().or_else(|| { self.delayed_highlights.take().map(|delayed_highlights| { Self::compute_runs(&self.text, &window.text_style(), delayed_highlights) }) }); let layout_id = self.layout.layout(self.text.clone(), runs, window, cx); (layout_id, ()) } fn prepaint( &mut self, _id: Option<&GlobalElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, _window: &mut Window, _cx: &mut App, ) { self.layout.prepaint(bounds, &self.text) } fn paint( &mut self, _id: Option<&GlobalElementId>, _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>, } impl TextLayout { 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 = text_style .line_height .to_pixels(font_size.into(), window.rem_size()); let mut runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] }; let layout_id = window.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space, window, cx| { let wrap_width = if text_style.white_space == WhiteSpace::Normal { known_dimensions.width.or(match available_space.width { crate::AvailableSpace::Definite(x) => Some(x), _ => None, }) } else { None }; let (truncate_width, ellipsis) = if let Some(text_overflow) = text_style.text_overflow { 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, }); match text_overflow { TextOverflow::Ellipsis(s) => (width, Some(s)), } } else { (None, None) }; if let Some(text_layout) = element_state.0.borrow().as_ref() { if text_layout.size.is_some() && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) { return text_layout.size.unwrap(); } } let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); let text = if let Some(truncate_width) = truncate_width { line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs) } else { text.clone() }; 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 } }); layout_id } 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.to_string()) .collect::>() .join("\n") } /// The text for this layout (with soft-wraps as newlines) pub fn wrapped_text(&self) -> String { let mut lines = Vec::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; lines.push(wrapped.text[seen..index].to_string()); seen = index; } lines.push(wrapped.text[seen..].to_string()); } lines.join("\n") } } /// 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 request_layout( &mut self, _id: Option<&GlobalElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { self.text.request_layout(None, window, cx) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, 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, bounds, state, window, cx); let hitbox = window.insert_hitbox(bounds, false); (hitbox, interactive_state) }, ) } fn paint( &mut self, global_id: Option<&GlobalElementId>, 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) { if self .clickable_ranges .iter() .any(|range| range.contains(&ix)) { window.set_cursor_style(crate::CursorStyle::PointingHand, Some(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) { if 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, bounds, &mut (), &mut (), window, cx); ((), interactive_state) }, ); } } impl IntoElement for InteractiveText { type Element = Self; fn into_element(self) -> Self::Element { self } }