diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 2bf478bef512418774e06d6e0ac67014557efb3f..43d1fecc585f185b24cd5d5f7de70d5965a230a3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9423,6 +9423,7 @@ impl Render for Editor { font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.).into(), + background_color: None, underline: None, white_space: WhiteSpace::Normal, }, @@ -9437,6 +9438,7 @@ impl Render for Editor { font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(settings.buffer_line_height.value()), + background_color: None, underline: None, white_space: WhiteSpace::Normal, }, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index da163bd8f3bf65803208e6442c3abd2702a73f2b..ab0477a9c4e6caa0f301d1f219f4b86b7fb90d0d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2452,7 +2452,7 @@ impl LineWithInvisibles { len: line_chunk.len(), font: text_style.font(), color: text_style.color, - background_color: None, + background_color: text_style.background_color, underline: text_style.underline, }); diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 74b74d0d05a22fa98de6262da6edaad3aa01e855..c81d4ff0edafc32271e3ececc4873aead286274c 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,11 +1,11 @@ use crate::{ - Bounds, Element, ElementId, IntoElement, LayoutId, Pixels, SharedString, Size, TextRun, - WhiteSpace, WindowContext, WrappedLine, + Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, + Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{cell::Cell, rc::Rc, sync::Arc}; +use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc}; use util::ResultExt; impl Element for &'static str { @@ -69,23 +69,28 @@ impl IntoElement for SharedString { } } +/// 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>, } impl StyledText { - /// 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 fn new(text: SharedString, runs: Vec) -> Self { + pub fn new(text: impl Into) -> Self { StyledText { - text, - runs: Some(runs), + text: text.into(), + runs: None, } } + + pub fn with_runs(mut self, runs: Vec) -> Self { + self.runs = Some(runs); + self + } } impl Element for StyledText { @@ -226,16 +231,73 @@ impl TextState { line_origin.y += line.size(line_height).height; } } + + fn index_for_position(&self, bounds: Bounds, position: Point) -> Option { + if !bounds.contains_point(&position) { + return None; + } + + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + 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; + } else { + let position_within_line = position - line_origin; + return line.index_for_position(position_within_line, line_height); + } + } + + None + } } -struct InteractiveText { +pub struct InteractiveText { element_id: ElementId, text: StyledText, + click_listener: Option)>>, } -struct InteractiveTextState { +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, +} + +pub struct InteractiveTextState { text_state: TextState, - clicked_range_ixs: Rc>>, + mouse_down_index: Rc>>, +} + +impl InteractiveText { + pub fn new(id: impl Into, text: StyledText) -> Self { + Self { + element_id: id.into(), + text, + click_listener: None, + } + } + + pub fn on_click( + mut self, + ranges: Vec>, + listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.click_listener = Some(Box::new(move |event, 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, cx); + } + } + })); + self + } } impl Element for InteractiveText { @@ -247,27 +309,62 @@ impl Element for InteractiveText { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { - text_state, - clicked_range_ixs, + mouse_down_index, .. }) = state { - let (layout_id, text_state) = self.text.layout(Some(text_state), cx); + let (layout_id, text_state) = self.text.layout(None, cx); let element_state = InteractiveTextState { text_state, - clicked_range_ixs, + mouse_down_index, }; (layout_id, element_state) } else { let (layout_id, text_state) = self.text.layout(None, cx); let element_state = InteractiveTextState { text_state, - clicked_range_ixs: Rc::default(), + mouse_down_index: Rc::default(), }; (layout_id, element_state) } } fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + if let Some(click_listener) = self.click_listener { + let text_state = state.text_state.clone(); + let mouse_down = state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + if let Some(mouse_up_index) = + text_state.index_for_position(bounds, event.position) + { + click_listener( + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + cx, + ) + } + + mouse_down.take(); + cx.notify(); + } + }); + } else { + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + if let Some(mouse_down_index) = + text_state.index_for_position(bounds, event.position) + { + mouse_down.set(Some(mouse_down_index)); + cx.notify(); + } + } + }); + } + } + self.text.paint(bounds, &mut state.text_state, cx) } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index c6f02f5bca85d7bfc2104d372b2c664cbeb30094..77d732032b29baeb08f5ec7d081f6012ab8d3de6 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -145,6 +145,7 @@ pub struct TextStyle { pub line_height: DefiniteLength, pub font_weight: FontWeight, pub font_style: FontStyle, + pub background_color: Option, pub underline: Option, pub white_space: WhiteSpace, } @@ -159,6 +160,7 @@ impl Default for TextStyle { line_height: phi(), font_weight: FontWeight::default(), font_style: FontStyle::default(), + background_color: None, underline: None, white_space: WhiteSpace::Normal, } @@ -182,6 +184,10 @@ impl TextStyle { self.color.fade_out(factor); } + if let Some(background_color) = style.background_color { + self.background_color = Some(background_color); + } + if let Some(underline) = style.underline { self.underline = Some(underline); } @@ -212,7 +218,7 @@ impl TextStyle { style: self.font_style, }, color: self.color, - background_color: None, + background_color: self.background_color, underline: self.underline.clone(), } } @@ -223,6 +229,7 @@ pub struct HighlightStyle { pub color: Option, pub font_weight: Option, pub font_style: Option, + pub background_color: Option, pub underline: Option, pub fade_out: Option, } @@ -441,6 +448,7 @@ impl From<&TextStyle> for HighlightStyle { color: Some(other.color), font_weight: Some(other.font_weight), font_style: Some(other.font_style), + background_color: other.background_color, underline: other.underline.clone(), fade_out: None, } @@ -467,6 +475,10 @@ impl HighlightStyle { self.font_style = other.font_style; } + if other.background_color.is_some() { + self.background_color = other.background_color; + } + if other.underline.is_some() { self.underline = other.underline; } diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index bdb9d4b4fe9e98f82f1e3aae1291f392bacd7ec5..77756154b58e1e0bd1c3bcbe709e0675c3bd6049 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -361,6 +361,13 @@ pub trait Styled: Sized { self } + fn text_bg(mut self, bg: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .background_color = Some(bg.into()); + self + } + fn text_size(mut self, size: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index b3d7a96aff9d2eaed8b5c9113be6b8d5f36b8873..76e21e5f7339cf2ac69e8caf3773e3456a56f3f8 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -196,7 +196,10 @@ impl TextSystem { let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); for run in runs { if let Some(last_run) = decoration_runs.last_mut() { - if last_run.color == run.color && last_run.underline == run.underline { + if last_run.color == run.color + && last_run.underline == run.underline + && last_run.background_color == run.background_color + { last_run.len += run.len as u32; continue; } @@ -204,6 +207,7 @@ impl TextSystem { decoration_runs.push(DecorationRun { len: run.len as u32, color: run.color, + background_color: run.background_color, underline: run.underline.clone(), }); } @@ -254,13 +258,16 @@ impl TextSystem { } if decoration_runs.last().map_or(false, |last_run| { - last_run.color == run.color && last_run.underline == run.underline + last_run.color == run.color + && last_run.underline == run.underline + && last_run.background_color == run.background_color }) { decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; } else { decoration_runs.push(DecorationRun { len: run_len_within_line as u32, color: run.color, + background_color: run.background_color, underline: run.underline.clone(), }); } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index d05ae9468dae491ed2ed130e6773fc1e754a3cf2..045a985ce7c1fbcb44d4afc6df88d9eee52ee75c 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -1,6 +1,7 @@ use crate::{ - black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, - UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, + black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, + LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, + WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -10,6 +11,7 @@ use std::sync::Arc; pub struct DecorationRun { pub len: u32, pub color: Hsla, + pub background_color: Option, pub underline: Option, } @@ -97,6 +99,7 @@ fn paint_line( let mut run_end = 0; let mut color = black(); let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let mut current_background: Option<(Point, Hsla)> = None; let text_system = cx.text_system().clone(); let mut glyph_origin = origin; let mut prev_glyph_position = Point::default(); @@ -110,12 +113,24 @@ fn paint_line( if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { wraps.next(); + if let Some((background_origin, background_color)) = current_background.take() { + cx.paint_quad( + Bounds { + origin: background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + Corners::default(), + background_color, + Edges::default(), + transparent_black(), + ); + } if let Some((underline_origin, underline_style)) = current_underline.take() { cx.paint_underline( underline_origin, glyph_origin.x - underline_origin.x, &underline_style, - )?; + ); } glyph_origin.x = origin.x; @@ -123,9 +138,20 @@ fn paint_line( } prev_glyph_position = glyph.position; + let mut finished_background: Option<(Point, Hsla)> = None; let mut finished_underline: Option<(Point, UnderlineStyle)> = None; if glyph.index >= run_end { if let Some(style_run) = decoration_runs.next() { + if let Some((_, background_color)) = &mut current_background { + if style_run.background_color.as_ref() != Some(background_color) { + finished_background = current_background.take(); + } + } + if let Some(run_background) = style_run.background_color { + current_background + .get_or_insert((point(glyph_origin.x, origin.y), run_background)); + } + if let Some((_, underline_style)) = &mut current_underline { if style_run.underline.as_ref() != Some(underline_style) { finished_underline = current_underline.take(); @@ -149,16 +175,30 @@ fn paint_line( color = style_run.color; } else { run_end = layout.len; + finished_background = current_background.take(); finished_underline = current_underline.take(); } } + if let Some((background_origin, background_color)) = finished_background { + cx.paint_quad( + Bounds { + origin: background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + Corners::default(), + background_color, + Edges::default(), + transparent_black(), + ); + } + if let Some((underline_origin, underline_style)) = finished_underline { cx.paint_underline( underline_origin, glyph_origin.x - underline_origin.x, &underline_style, - )?; + ); } let max_glyph_bounds = Bounds { @@ -188,13 +228,27 @@ fn paint_line( } } + if let Some((background_origin, background_color)) = current_background.take() { + let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); + cx.paint_quad( + Bounds { + origin: background_origin, + size: size(line_end_x - background_origin.x, line_height), + }, + Corners::default(), + background_color, + Edges::default(), + transparent_black(), + ); + } + if let Some((underline_start, underline_style)) = current_underline.take() { let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); cx.paint_underline( underline_start, line_end_x - underline_start.x, &underline_style, - )?; + ); } Ok(()) diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index a5cf814a8c24e36d5c41217f7db5aa528452d312..2370aca83b0c04874c71d738b7e26a21ad272ccd 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -198,6 +198,41 @@ impl WrappedLineLayout { pub fn runs(&self) -> &[ShapedRun] { &self.unwrapped_layout.runs } + + pub fn index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Option { + let wrapped_line_ix = (position.y / line_height) as usize; + + let wrapped_line_start_x = if wrapped_line_ix > 0 { + let wrap_boundary_ix = wrapped_line_ix - 1; + let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; + run.glyphs[wrap_boundary.glyph_ix].position.x + } else { + Pixels::ZERO + }; + + let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() { + let next_wrap_boundary_ix = wrapped_line_ix; + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; + run.glyphs[next_wrap_boundary.glyph_ix].position.x + } else { + self.unwrapped_layout.width + }; + + let mut position_in_unwrapped_line = position; + position_in_unwrapped_line.x += wrapped_line_start_x; + if position_in_unwrapped_line.x > wrapped_line_end_x { + None + } else { + self.unwrapped_layout + .index_for_x(position_in_unwrapped_line.x) + } + } } pub(crate) struct LineLayoutCache { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 1973aa14a98dc258ee81e0a823e7f1be515f91eb..7b39089ae0773295f8cf5d34090acdaa67f10003 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -881,7 +881,7 @@ impl<'a> WindowContext<'a> { origin: Point, width: Pixels, style: &UnderlineStyle, - ) -> Result<()> { + ) { let scale_factor = self.scale_factor(); let height = if style.wavy { style.thickness * 3. @@ -905,7 +905,6 @@ impl<'a> WindowContext<'a> { wavy: style.wavy, }, ); - Ok(()) } /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index c26e5fd3f1adc78aa3e9e8bf99f8c8f26666db78..42009136c414750de416dd3272e82955cb17ef60 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,5 +1,6 @@ use gpui::{ - blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext, + blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, + TextRun, View, VisualContext, WindowContext, }; use ui::v_stack; @@ -55,6 +56,21 @@ impl Render for TextStory { "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", "Meanwhile, the lazy dog decided it was time for a change. ", "He started daily workout routines, ate healthier and became the fastest dog in town.", - ))) + ))).child( + InteractiveText::new( + "interactive", + StyledText::new("Hello world, how is it going?").with_runs(vec![ + cx.text_style().to_run(6), + TextRun { + background_color: Some(green()), + ..cx.text_style().to_run(5) + }, + cx.text_style().to_run(18), + ]), + ) + .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| { + println!("Clicked range {range_ix}"); + }) + ) } } diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index eafb6d55bb815f66544909f19347fce90e98596b..627a95d9534d2a4aadda019d6be59b2977653ed3 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -150,7 +150,7 @@ impl RenderOnce for HighlightedLabel { LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), }) - .child(StyledText::new(self.label, runs)) + .child(StyledText::new(self.label).with_runs(runs)) } }