use crate::{ color::Color, fonts::{HighlightStyle, TextStyle}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, text_layout::{Line, RunStyle, ShapedBoundary}, Element, FontCache, SizeConstraint, TextLayoutCache, ViewContext, WindowContext, }; use log::warn; use serde_json::json; use std::{borrow::Cow, ops::Range, sync::Arc}; pub struct Text { text: Cow<'static, str>, style: TextStyle, soft_wrap: bool, highlights: Option, HighlightStyle)]>>, custom_runs: Option<( Box<[Range]>, Box, )>, } pub struct LayoutState { shaped_lines: Vec, wrap_boundaries: Vec>, line_height: f32, } impl Text { pub fn new>>(text: I, style: TextStyle) -> Self { Self { text: text.into(), style, soft_wrap: true, highlights: None, custom_runs: None, } } pub fn with_default_color(mut self, color: Color) -> Self { self.style.color = color; self } pub fn with_highlights( mut self, runs: impl Into, HighlightStyle)]>>, ) -> Self { self.highlights = Some(runs.into()); self } pub fn with_custom_runs( mut self, runs: impl Into]>>, callback: impl 'static + FnMut(usize, RectF, &mut WindowContext), ) -> Self { self.custom_runs = Some((runs.into(), Box::new(callback))); self } pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self { self.soft_wrap = soft_wrap; self } } impl Element for Text { type LayoutState = LayoutState; type PaintState = (); fn layout( &mut self, constraint: SizeConstraint, _: &mut V, cx: &mut ViewContext, ) -> (Vector2F, Self::LayoutState) { // Convert the string and highlight ranges into an iterator of highlighted chunks. let mut offset = 0; let mut highlight_ranges = self .highlights .as_ref() .map_or(Default::default(), AsRef::as_ref) .iter() .peekable(); let chunks = std::iter::from_fn(|| { let result; if let Some((range, highlight_style)) = highlight_ranges.peek() { if offset < range.start { result = Some((&self.text[offset..range.start], None)); offset = range.start; } else if range.end <= self.text.len() { result = Some((&self.text[range.clone()], Some(*highlight_style))); highlight_ranges.next(); offset = range.end; } else { warn!( "Highlight out of text range. Text len: {}, Highlight range: {}..{}", self.text.len(), range.start, range.end ); result = None; } } else if offset < self.text.len() { result = Some((&self.text[offset..], None)); offset = self.text.len(); } else { result = None; } result }); // Perform shaping on these highlighted chunks let shaped_lines = layout_highlighted_chunks( chunks, &self.style, cx.text_layout_cache(), &cx.font_cache, usize::MAX, self.text.matches('\n').count() + 1, ); // If line wrapping is enabled, wrap each of the shaped lines. let font_id = self.style.font_id; let mut line_count = 0; let mut max_line_width = 0_f32; let mut wrap_boundaries = Vec::new(); let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size); for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) { if self.soft_wrap { let boundaries = wrapper .wrap_shaped_line(line, shaped_line, constraint.max.x()) .collect::>(); line_count += boundaries.len() + 1; wrap_boundaries.push(boundaries); } else { line_count += 1; } max_line_width = max_line_width.max(shaped_line.width()); } let line_height = cx.font_cache.line_height(self.style.font_size); let size = vec2f( max_line_width .ceil() .max(constraint.min.x()) .min(constraint.max.x()), (line_height * line_count as f32).ceil(), ); ( size, LayoutState { shaped_lines, wrap_boundaries, line_height, }, ) } fn paint( &mut self, bounds: RectF, visible_bounds: RectF, layout: &mut Self::LayoutState, _: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { let mut origin = bounds.origin(); let empty = Vec::new(); let mut callback = |_, _, _: &mut WindowContext| {}; let mouse_runs; let custom_run_callback; if let Some((runs, build_region)) = &mut self.custom_runs { mouse_runs = runs.iter(); custom_run_callback = build_region.as_mut(); } else { mouse_runs = [].iter(); custom_run_callback = &mut callback; } let mut custom_runs = mouse_runs.enumerate().peekable(); let mut offset = 0; for (ix, line) in layout.shaped_lines.iter().enumerate() { let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty); let boundaries = RectF::new( origin, vec2f( bounds.width(), (wrap_boundaries.len() + 1) as f32 * layout.line_height, ), ); if boundaries.intersects(visible_bounds) { if self.soft_wrap { line.paint_wrapped( origin, visible_bounds, layout.line_height, wrap_boundaries, cx, ); } else { line.paint(origin, visible_bounds, layout.line_height, cx); } } // Paint any custom runs that intersect this line. let end_offset = offset + line.len(); if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() { if custom_run_range.start < end_offset { let mut current_custom_run = None; if custom_run_range.start <= offset { current_custom_run = Some((custom_run_ix, custom_run_range.end, origin)); } let mut glyph_origin = origin; let mut prev_position = 0.; let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable(); for (run_ix, glyph_ix, glyph) in line.runs().iter().enumerate().flat_map(|(run_ix, run)| { run.glyphs() .iter() .enumerate() .map(move |(ix, glyph)| (run_ix, ix, glyph)) }) { glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); prev_position = glyph.position.x(); // If we've reached a soft wrap position, move down one line. If there // is a custom run in-progress, paint it. if wrap_boundaries .peek() .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix) { if let Some((run_ix, _, run_origin)) = &mut current_custom_run { let bounds = RectF::from_points( *run_origin, glyph_origin + vec2f(0., layout.line_height), ); custom_run_callback(*run_ix, bounds, cx); *run_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); } wrap_boundaries.next(); glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); } // If we've reached the end of the current custom run, paint it. if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run { if offset + glyph.index == run_end_offset { current_custom_run.take(); let bounds = RectF::from_points( run_origin, glyph_origin + vec2f(0., layout.line_height), ); custom_run_callback(run_ix, bounds, cx); custom_runs.next(); } if let Some((_, run_range)) = custom_runs.peek() { if run_range.start >= end_offset { break; } if run_range.start == offset + glyph.index { current_custom_run = Some((run_ix, run_range.end, glyph_origin)); } } } // If we've reached the start of a new custom run, start tracking it. if let Some((run_ix, run_range)) = custom_runs.peek() { if offset + glyph.index == run_range.start { current_custom_run = Some((*run_ix, run_range.end, glyph_origin)); } } } // If a custom run extends beyond the end of the line, paint it. if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run { let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.); let bounds = RectF::from_points( run_origin, line_end + vec2f(0., layout.line_height), ); custom_run_callback(run_ix, bounds, cx); if end_offset == run_end_offset { custom_runs.next(); } } } } offset = end_offset + 1; origin.set_y(boundaries.max_y()); } } fn rect_for_text_range( &self, _: Range, _: RectF, _: RectF, _: &Self::LayoutState, _: &Self::PaintState, _: &V, _: &ViewContext, ) -> Option { None } fn debug( &self, bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, _: &V, _: &ViewContext, ) -> Value { json!({ "type": "Text", "bounds": bounds.to_json(), "text": &self.text, "style": self.style.to_json(), }) } } /// Perform text layout on a series of highlighted chunks of text. pub fn layout_highlighted_chunks<'a>( chunks: impl Iterator)>, text_style: &TextStyle, text_layout_cache: &TextLayoutCache, font_cache: &Arc, max_line_len: usize, max_line_count: usize, ) -> Vec { let mut layouts = Vec::with_capacity(max_line_count); let mut line = String::new(); let mut styles = Vec::new(); let mut row = 0; let mut line_exceeded_max_len = false; for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) { for (ix, mut line_chunk) in chunk.split('\n').enumerate() { if ix > 0 { layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles)); line.clear(); styles.clear(); row += 1; line_exceeded_max_len = false; if row == max_line_count { return layouts; } } if !line_chunk.is_empty() && !line_exceeded_max_len { let text_style = if let Some(style) = highlight_style { text_style .clone() .highlight(style, font_cache) .map(Cow::Owned) .unwrap_or_else(|_| Cow::Borrowed(text_style)) } else { Cow::Borrowed(text_style) }; if line.len() + line_chunk.len() > max_line_len { let mut chunk_len = max_line_len - line.len(); while !line_chunk.is_char_boundary(chunk_len) { chunk_len -= 1; } line_chunk = &line_chunk[..chunk_len]; line_exceeded_max_len = true; } line.push_str(line_chunk); styles.push(( line_chunk.len(), RunStyle { font_id: text_style.font_id, color: text_style.color, underline: text_style.underline, }, )); } } } layouts } #[cfg(test)] mod tests { use super::*; use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext}; #[crate::test(self)] fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) { cx.add_window(Default::default(), |cx| { let mut view = TestView; fonts::with_font_cache(cx.font_cache().clone(), || { let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true); let (_, state) = text.layout( SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)), &mut view, cx, ); assert_eq!(state.shaped_lines.len(), 2); assert_eq!(state.wrap_boundaries.len(), 2); }); view }); } struct TestView; impl Entity for TestView { type Event = (); } impl View for TestView { fn ui_name() -> &'static str { "TestView" } fn render(&mut self, _: &mut ViewContext) -> AnyElement { Empty::new().into_any() } } }