diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 10aa61fde5..f60e74deb1 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -1,13 +1,12 @@ use gpui::{ color::Color, - fonts::{Properties, Weight}, - text_layout::RunStyle, - AnyElement, Element, Quad, SceneBuilder, View, ViewContext, + elements::Text, + fonts::{HighlightStyle, TextStyle}, + platform::MouseButton, + AnyElement, Element, MouseRegion, }; use log::LevelFilter; -use pathfinder_geometry::rect::RectF; use simplelog::SimpleLogger; -use std::ops::Range; fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); @@ -19,7 +18,6 @@ fn main() { } struct TextView; -struct TextElement; impl gpui::Entity for TextView { type Event = (); @@ -30,104 +28,47 @@ impl gpui::View for TextView { "View" } - fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { - TextElement.into_any() - } -} - -impl Element for TextElement { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut V, - _: &mut ViewContext, - ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } - - fn paint( - &mut self, - scene: &mut SceneBuilder, - bounds: RectF, - visible_bounds: RectF, - _: &mut Self::LayoutState, - _: &mut V, - cx: &mut ViewContext, - ) -> Self::PaintState { + fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { let font_size = 12.; let family = cx .font_cache - .load_family(&["SF Pro Display"], &Default::default()) + .load_family(&["Monaco"], &Default::default()) .unwrap(); - let normal = RunStyle { - font_id: cx - .font_cache - .select_font(family, &Default::default()) - .unwrap(), - color: Color::default(), - underline: Default::default(), - }; - let bold = RunStyle { - font_id: cx - .font_cache - .select_font( - family, - &Properties { - weight: Weight::BOLD, - ..Default::default() - }, - ) - .unwrap(), - color: Color::default(), - underline: Default::default(), - }; + let font_id = cx + .font_cache + .select_font(family, &Default::default()) + .unwrap(); + let view_id = cx.view_id(); - let text = "Hello world!"; - let line = cx.text_layout_cache().layout_str( - text, - font_size, - &[ - (1, normal), - (1, bold), - (1, normal), - (1, bold), - (text.len() - 4, normal), - ], - ); - - scene.push_quad(Quad { - bounds, - background: Some(Color::white()), + let underline = HighlightStyle { + underline: Some(gpui::fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - }); - line.paint(scene, bounds.origin(), visible_bounds, bounds.height(), cx); - } + }; - fn rect_for_text_range( - &self, - _: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> Option { - None - } - - fn debug( - &self, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> gpui::json::Value { - todo!() + Text::new( + "The text:\nHello, beautiful world, hello!", + TextStyle { + font_id, + font_size, + color: Color::red(), + font_family_name: "".into(), + font_family_id: family, + underline: Default::default(), + font_properties: Default::default(), + }, + ) + .with_highlights(vec![(17..26, underline), (34..40, underline)]) + .with_mouse_regions(vec![(17..26), (34..40)], move |ix, bounds| { + MouseRegion::new::(view_id, ix, bounds).on_click::( + MouseButton::Left, + move |_, _, _| { + eprintln!("clicked link {ix}"); + }, + ) + }) + .into_any() } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 3090a81c72..357bce9d0d 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -6,8 +6,10 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{ToJson, Value}, + platform::CursorStyle, text_layout::{Line, RunStyle, ShapedBoundary}, - Element, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View, ViewContext, + CursorRegion, Element, FontCache, MouseRegion, SceneBuilder, SizeConstraint, TextLayoutCache, + View, ViewContext, }; use log::warn; use serde_json::json; @@ -17,7 +19,11 @@ pub struct Text { text: Cow<'static, str>, style: TextStyle, soft_wrap: bool, - highlights: Vec<(Range, HighlightStyle)>, + highlights: Option, HighlightStyle)]>>, + mouse_runs: Option<( + Box<[Range]>, + Box MouseRegion>, + )>, } pub struct LayoutState { @@ -32,7 +38,8 @@ impl Text { text: text.into(), style, soft_wrap: true, - highlights: Vec::new(), + highlights: None, + mouse_runs: None, } } @@ -41,8 +48,20 @@ impl Text { self } - pub fn with_highlights(mut self, runs: Vec<(Range, HighlightStyle)>) -> Self { - self.highlights = runs; + pub fn with_highlights( + mut self, + runs: impl Into, HighlightStyle)]>>, + ) -> Self { + self.highlights = Some(runs.into()); + self + } + + pub fn with_mouse_regions( + mut self, + runs: impl Into]>>, + build_mouse_region: impl 'static + FnMut(usize, RectF) -> MouseRegion, + ) -> Self { + self.mouse_runs = Some((runs.into(), Box::new(build_mouse_region))); self } @@ -65,7 +84,12 @@ impl Element for Text { // Convert the string and highlight ranges into an iterator of highlighted chunks. let mut offset = 0; - let mut highlight_ranges = self.highlights.iter().peekable(); + 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() { @@ -152,6 +176,19 @@ impl Element for Text { ) -> Self::PaintState { let mut origin = bounds.origin(); let empty = Vec::new(); + + let mouse_runs; + let mut build_mouse_region; + if let Some((runs, build_region)) = &mut self.mouse_runs { + mouse_runs = runs.iter(); + build_mouse_region = Some(build_region); + } else { + mouse_runs = [].iter(); + build_mouse_region = None; + } + let mut mouse_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( @@ -169,13 +206,114 @@ impl Element for Text { origin, visible_bounds, layout.line_height, - wrap_boundaries.iter().copied(), + wrap_boundaries, cx, ); } else { line.paint(scene, origin, visible_bounds, layout.line_height, cx); } } + + // Add the mouse regions + let end_offset = offset + line.len(); + if let Some((mut mouse_run_ix, mut mouse_run_range)) = mouse_runs.peek().cloned() { + if mouse_run_range.start < end_offset { + let mut current_mouse_run = None; + if mouse_run_range.start <= offset { + current_mouse_run = Some((mouse_run_ix, origin)); + } + + let mut glyph_origin = origin; + let mut prev_position = 0.; + let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable(); + for (glyph_ix, glyph) in line + .runs() + .iter() + .flat_map(|run| run.glyphs().iter().enumerate()) + { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + prev_position = glyph.position.x(); + + if wrap_boundaries + .peek() + .map_or(false, |b| b.glyph_ix == glyph_ix) + { + if let Some((mouse_run_ix, mouse_region_start)) = &mut current_mouse_run + { + let bounds = RectF::from_points( + *mouse_region_start, + glyph_origin + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + *mouse_run_ix, + bounds, + )); + *mouse_region_start = + vec2f(origin.x(), glyph_origin.y() + layout.line_height); + } + + wrap_boundaries.next(); + glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height); + } + + if offset + glyph.index == mouse_run_range.start { + current_mouse_run = Some((mouse_run_ix, glyph_origin)); + } + if offset + glyph.index == mouse_run_range.end { + if let Some((mouse_run_ix, mouse_region_start)) = + current_mouse_run.take() + { + let bounds = RectF::from_points( + mouse_region_start, + glyph_origin + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + mouse_run_ix, + bounds, + )); + mouse_runs.next(); + } + + if let Some(next) = mouse_runs.peek() { + mouse_run_ix = next.0; + mouse_run_range = next.1; + if mouse_run_range.start >= end_offset { + break; + } + if mouse_run_range.start == offset + glyph.index { + current_mouse_run = Some((mouse_run_ix, glyph_origin)); + } + } + } + } + + if let Some((mouse_run_ix, mouse_region_start)) = current_mouse_run { + let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.); + let bounds = RectF::from_points( + mouse_region_start, + line_end + vec2f(0., layout.line_height), + ); + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region((build_mouse_region.as_mut().unwrap())( + mouse_run_ix, + bounds, + )); + } + } + } + + offset = end_offset + 1; origin.set_y(boundaries.max_y()); } } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index b557afc319..3f2f7890b8 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -393,41 +393,82 @@ impl Line { origin: Vector2F, visible_bounds: RectF, line_height: f32, - boundaries: impl IntoIterator, + boundaries: &[ShapedBoundary], cx: &mut WindowContext, ) { let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.; - let baseline_origin = vec2f(0., padding_top + self.layout.ascent); + let baseline_offset = vec2f(0., padding_top + self.layout.ascent); let mut boundaries = boundaries.into_iter().peekable(); let mut color_runs = self.style_runs.iter(); - let mut color_end = 0; + let mut style_run_end = 0; let mut color = Color::black(); + let mut underline: Option<(Vector2F, Underline)> = None; - let mut glyph_origin = vec2f(0., 0.); + let mut glyph_origin = origin; let mut prev_position = 0.; for run in &self.layout.runs { for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + if boundaries.peek().map_or(false, |b| b.glyph_ix == glyph_ix) { boundaries.next(); - glyph_origin = vec2f(0., glyph_origin.y() + line_height); - } else { - glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position); + if let Some((underline_origin, underline_style)) = underline { + scene.push_underline(scene::Underline { + origin: underline_origin, + width: glyph_origin.x() - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } + + glyph_origin = vec2f(origin.x(), glyph_origin.y() + line_height); } prev_position = glyph.position.x(); - if glyph.index >= color_end { - if let Some(next_run) = color_runs.next() { - color_end += next_run.len as usize; - color = next_run.color; + let mut finished_underline = None; + if glyph.index >= style_run_end { + if let Some(style_run) = color_runs.next() { + style_run_end += style_run.len as usize; + color = style_run.color; + if let Some((_, underline_style)) = underline { + if style_run.underline != underline_style { + finished_underline = underline.take(); + } + } + if style_run.underline.thickness.into_inner() > 0. { + underline.get_or_insert(( + glyph_origin + + vec2f(0., baseline_offset.y() + 0.618 * self.layout.descent), + Underline { + color: Some( + style_run.underline.color.unwrap_or(style_run.color), + ), + thickness: style_run.underline.thickness, + squiggly: style_run.underline.squiggly, + }, + )); + } } else { - color_end = self.layout.len; + style_run_end = self.layout.len; color = Color::black(); + finished_underline = underline.take(); } } + if let Some((underline_origin, underline_style)) = finished_underline { + scene.push_underline(scene::Underline { + origin: underline_origin, + width: glyph_origin.x() - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } + let glyph_bounds = RectF::new( - origin + glyph_origin, + glyph_origin, cx.font_cache .bounding_box(run.font_id, self.layout.font_size), ); @@ -437,20 +478,31 @@ impl Line { font_id: run.font_id, font_size: self.layout.font_size, id: glyph.id, - origin: glyph_bounds.origin() + baseline_origin, + origin: glyph_bounds.origin() + baseline_offset, }); } else { scene.push_glyph(scene::Glyph { font_id: run.font_id, font_size: self.layout.font_size, id: glyph.id, - origin: glyph_bounds.origin() + baseline_origin, + origin: glyph_bounds.origin() + baseline_offset, color, }); } } } } + + if let Some((underline_origin, underline_style)) = underline.take() { + let line_end_x = glyph_origin.x() + self.layout.width - prev_position; + scene.push_underline(scene::Underline { + origin: underline_origin, + width: line_end_x - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color.unwrap(), + squiggly: underline_style.squiggly, + }); + } } }