Detailed changes
@@ -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<Self>) -> AnyElement<TextView> {
- TextElement.into_any()
- }
-}
-
-impl<V: View> Element<V> for TextElement {
- type LayoutState = ();
-
- type PaintState = ();
-
- fn layout(
- &mut self,
- constraint: gpui::SizeConstraint,
- _: &mut V,
- _: &mut ViewContext<V>,
- ) -> (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<V>,
- ) -> Self::PaintState {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<TextView> {
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 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),
- ],
- );
+ let font_id = cx
+ .font_cache
+ .select_font(family, &Default::default())
+ .unwrap();
+ let view_id = cx.view_id();
- 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<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &V,
- _: &ViewContext<V>,
- ) -> Option<RectF> {
- None
- }
+ };
- fn debug(
- &self,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &V,
- _: &ViewContext<V>,
- ) -> 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::<Self>(view_id, ix, bounds).on_click::<Self, _>(
+ MouseButton::Left,
+ move |_, _, _| {
+ eprintln!("clicked link {ix}");
+ },
+ )
+ })
+ .into_any()
}
}
@@ -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<usize>, HighlightStyle)>,
+ highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
+ mouse_runs: Option<(
+ Box<[Range<usize>]>,
+ Box<dyn FnMut(usize, RectF) -> 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<usize>, HighlightStyle)>) -> Self {
- self.highlights = runs;
+ pub fn with_highlights(
+ mut self,
+ runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
+ ) -> Self {
+ self.highlights = Some(runs.into());
+ self
+ }
+
+ pub fn with_mouse_regions(
+ mut self,
+ runs: impl Into<Box<[Range<usize>]>>,
+ 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<V: View> Element<V> 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<V: View> Element<V> 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<V: View> Element<V> 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());
}
}
@@ -393,41 +393,82 @@ impl Line {
origin: Vector2F,
visible_bounds: RectF,
line_height: f32,
- boundaries: impl IntoIterator<Item = ShapedBoundary>,
+ 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,
+ });
+ }
}
}