Introduce InteractiveText
(#3397)
This new element will let us react to click events on arbitrary ranges of some rendered text, e.g.: ```rs InteractiveText::new( "element-id", 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}"); }) ``` As part of this, I also added the ability to give text runs a background color. Release Notes: - N/A
This commit is contained in:
commit
510320bb47
11 changed files with 262 additions and 33 deletions
|
@ -9423,6 +9423,7 @@ impl Render for Editor {
|
||||||
font_weight: FontWeight::NORMAL,
|
font_weight: FontWeight::NORMAL,
|
||||||
font_style: FontStyle::Normal,
|
font_style: FontStyle::Normal,
|
||||||
line_height: relative(1.).into(),
|
line_height: relative(1.).into(),
|
||||||
|
background_color: None,
|
||||||
underline: None,
|
underline: None,
|
||||||
white_space: WhiteSpace::Normal,
|
white_space: WhiteSpace::Normal,
|
||||||
},
|
},
|
||||||
|
@ -9437,6 +9438,7 @@ impl Render for Editor {
|
||||||
font_weight: FontWeight::NORMAL,
|
font_weight: FontWeight::NORMAL,
|
||||||
font_style: FontStyle::Normal,
|
font_style: FontStyle::Normal,
|
||||||
line_height: relative(settings.buffer_line_height.value()),
|
line_height: relative(settings.buffer_line_height.value()),
|
||||||
|
background_color: None,
|
||||||
underline: None,
|
underline: None,
|
||||||
white_space: WhiteSpace::Normal,
|
white_space: WhiteSpace::Normal,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2452,7 +2452,7 @@ impl LineWithInvisibles {
|
||||||
len: line_chunk.len(),
|
len: line_chunk.len(),
|
||||||
font: text_style.font(),
|
font: text_style.font(),
|
||||||
color: text_style.color,
|
color: text_style.color,
|
||||||
background_color: None,
|
background_color: text_style.background_color,
|
||||||
underline: text_style.underline,
|
underline: text_style.underline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
Bounds, Element, ElementId, IntoElement, LayoutId, Pixels, SharedString, Size, TextRun,
|
Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
|
||||||
WhiteSpace, WindowContext, WrappedLine,
|
Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use parking_lot::{Mutex, MutexGuard};
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cell::Cell, rc::Rc, sync::Arc};
|
use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
impl Element for &'static str {
|
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 {
|
pub struct StyledText {
|
||||||
text: SharedString,
|
text: SharedString,
|
||||||
runs: Option<Vec<TextRun>>,
|
runs: Option<Vec<TextRun>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyledText {
|
impl StyledText {
|
||||||
/// Renders text with runs of different styles.
|
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||||
///
|
|
||||||
/// 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<TextRun>) -> Self {
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text,
|
text: text.into(),
|
||||||
runs: Some(runs),
|
runs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||||
|
self.runs = Some(runs);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for StyledText {
|
impl Element for StyledText {
|
||||||
|
@ -226,16 +231,73 @@ impl TextState {
|
||||||
line_origin.y += line.size(line_height).height;
|
line_origin.y += line.size(line_height).height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||||
|
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,
|
element_id: ElementId,
|
||||||
text: StyledText,
|
text: StyledText,
|
||||||
|
click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InteractiveTextState {
|
struct InteractiveTextClickEvent {
|
||||||
|
mouse_down_index: usize,
|
||||||
|
mouse_up_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InteractiveTextState {
|
||||||
text_state: TextState,
|
text_state: TextState,
|
||||||
clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
|
mouse_down_index: Rc<Cell<Option<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractiveText {
|
||||||
|
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
||||||
|
Self {
|
||||||
|
element_id: id.into(),
|
||||||
|
text,
|
||||||
|
click_listener: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_click(
|
||||||
|
mut self,
|
||||||
|
ranges: Vec<Range<usize>>,
|
||||||
|
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 {
|
impl Element for InteractiveText {
|
||||||
|
@ -247,27 +309,62 @@ impl Element for InteractiveText {
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, Self::State) {
|
) -> (LayoutId, Self::State) {
|
||||||
if let Some(InteractiveTextState {
|
if let Some(InteractiveTextState {
|
||||||
text_state,
|
mouse_down_index, ..
|
||||||
clicked_range_ixs,
|
|
||||||
}) = state
|
}) = 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 {
|
let element_state = InteractiveTextState {
|
||||||
text_state,
|
text_state,
|
||||||
clicked_range_ixs,
|
mouse_down_index,
|
||||||
};
|
};
|
||||||
(layout_id, element_state)
|
(layout_id, element_state)
|
||||||
} else {
|
} else {
|
||||||
let (layout_id, text_state) = self.text.layout(None, cx);
|
let (layout_id, text_state) = self.text.layout(None, cx);
|
||||||
let element_state = InteractiveTextState {
|
let element_state = InteractiveTextState {
|
||||||
text_state,
|
text_state,
|
||||||
clicked_range_ixs: Rc::default(),
|
mouse_down_index: Rc::default(),
|
||||||
};
|
};
|
||||||
(layout_id, element_state)
|
(layout_id, element_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
fn paint(self, bounds: Bounds<Pixels>, 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)
|
self.text.paint(bounds, &mut state.text_state, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,7 @@ pub struct TextStyle {
|
||||||
pub line_height: DefiniteLength,
|
pub line_height: DefiniteLength,
|
||||||
pub font_weight: FontWeight,
|
pub font_weight: FontWeight,
|
||||||
pub font_style: FontStyle,
|
pub font_style: FontStyle,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
pub white_space: WhiteSpace,
|
pub white_space: WhiteSpace,
|
||||||
}
|
}
|
||||||
|
@ -159,6 +160,7 @@ impl Default for TextStyle {
|
||||||
line_height: phi(),
|
line_height: phi(),
|
||||||
font_weight: FontWeight::default(),
|
font_weight: FontWeight::default(),
|
||||||
font_style: FontStyle::default(),
|
font_style: FontStyle::default(),
|
||||||
|
background_color: None,
|
||||||
underline: None,
|
underline: None,
|
||||||
white_space: WhiteSpace::Normal,
|
white_space: WhiteSpace::Normal,
|
||||||
}
|
}
|
||||||
|
@ -182,6 +184,10 @@ impl TextStyle {
|
||||||
self.color.fade_out(factor);
|
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 {
|
if let Some(underline) = style.underline {
|
||||||
self.underline = Some(underline);
|
self.underline = Some(underline);
|
||||||
}
|
}
|
||||||
|
@ -212,7 +218,7 @@ impl TextStyle {
|
||||||
style: self.font_style,
|
style: self.font_style,
|
||||||
},
|
},
|
||||||
color: self.color,
|
color: self.color,
|
||||||
background_color: None,
|
background_color: self.background_color,
|
||||||
underline: self.underline.clone(),
|
underline: self.underline.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +229,7 @@ pub struct HighlightStyle {
|
||||||
pub color: Option<Hsla>,
|
pub color: Option<Hsla>,
|
||||||
pub font_weight: Option<FontWeight>,
|
pub font_weight: Option<FontWeight>,
|
||||||
pub font_style: Option<FontStyle>,
|
pub font_style: Option<FontStyle>,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
pub fade_out: Option<f32>,
|
pub fade_out: Option<f32>,
|
||||||
}
|
}
|
||||||
|
@ -441,6 +448,7 @@ impl From<&TextStyle> for HighlightStyle {
|
||||||
color: Some(other.color),
|
color: Some(other.color),
|
||||||
font_weight: Some(other.font_weight),
|
font_weight: Some(other.font_weight),
|
||||||
font_style: Some(other.font_style),
|
font_style: Some(other.font_style),
|
||||||
|
background_color: other.background_color,
|
||||||
underline: other.underline.clone(),
|
underline: other.underline.clone(),
|
||||||
fade_out: None,
|
fade_out: None,
|
||||||
}
|
}
|
||||||
|
@ -467,6 +475,10 @@ impl HighlightStyle {
|
||||||
self.font_style = other.font_style;
|
self.font_style = other.font_style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if other.background_color.is_some() {
|
||||||
|
self.background_color = other.background_color;
|
||||||
|
}
|
||||||
|
|
||||||
if other.underline.is_some() {
|
if other.underline.is_some() {
|
||||||
self.underline = other.underline;
|
self.underline = other.underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -361,6 +361,13 @@ pub trait Styled: Sized {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
|
||||||
|
self.text_style()
|
||||||
|
.get_or_insert_with(Default::default)
|
||||||
|
.background_color = Some(bg.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
||||||
self.text_style()
|
self.text_style()
|
||||||
.get_or_insert_with(Default::default)
|
.get_or_insert_with(Default::default)
|
||||||
|
|
|
@ -196,7 +196,10 @@ impl TextSystem {
|
||||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||||
for run in runs {
|
for run in runs {
|
||||||
if let Some(last_run) = decoration_runs.last_mut() {
|
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;
|
last_run.len += run.len as u32;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +207,7 @@ impl TextSystem {
|
||||||
decoration_runs.push(DecorationRun {
|
decoration_runs.push(DecorationRun {
|
||||||
len: run.len as u32,
|
len: run.len as u32,
|
||||||
color: run.color,
|
color: run.color,
|
||||||
|
background_color: run.background_color,
|
||||||
underline: run.underline.clone(),
|
underline: run.underline.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -254,13 +258,16 @@ impl TextSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
if decoration_runs.last().map_or(false, |last_run| {
|
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;
|
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
|
||||||
} else {
|
} else {
|
||||||
decoration_runs.push(DecorationRun {
|
decoration_runs.push(DecorationRun {
|
||||||
len: run_len_within_line as u32,
|
len: run_len_within_line as u32,
|
||||||
color: run.color,
|
color: run.color,
|
||||||
|
background_color: run.background_color,
|
||||||
underline: run.underline.clone(),
|
underline: run.underline.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
|
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
|
||||||
UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
|
||||||
|
WrappedLineLayout,
|
||||||
};
|
};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -10,6 +11,7 @@ use std::sync::Arc;
|
||||||
pub struct DecorationRun {
|
pub struct DecorationRun {
|
||||||
pub len: u32,
|
pub len: u32,
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
pub underline: Option<UnderlineStyle>,
|
pub underline: Option<UnderlineStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +99,7 @@ fn paint_line(
|
||||||
let mut run_end = 0;
|
let mut run_end = 0;
|
||||||
let mut color = black();
|
let mut color = black();
|
||||||
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
|
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
let text_system = cx.text_system().clone();
|
let text_system = cx.text_system().clone();
|
||||||
let mut glyph_origin = origin;
|
let mut glyph_origin = origin;
|
||||||
let mut prev_glyph_position = Point::default();
|
let mut prev_glyph_position = Point::default();
|
||||||
|
@ -110,12 +113,24 @@ fn paint_line(
|
||||||
|
|
||||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||||
wraps.next();
|
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() {
|
if let Some((underline_origin, underline_style)) = current_underline.take() {
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_origin,
|
underline_origin,
|
||||||
glyph_origin.x - underline_origin.x,
|
glyph_origin.x - underline_origin.x,
|
||||||
&underline_style,
|
&underline_style,
|
||||||
)?;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
glyph_origin.x = origin.x;
|
glyph_origin.x = origin.x;
|
||||||
|
@ -123,9 +138,20 @@ fn paint_line(
|
||||||
}
|
}
|
||||||
prev_glyph_position = glyph.position;
|
prev_glyph_position = glyph.position;
|
||||||
|
|
||||||
|
let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
|
||||||
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
|
||||||
if glyph.index >= run_end {
|
if glyph.index >= run_end {
|
||||||
if let Some(style_run) = decoration_runs.next() {
|
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 let Some((_, underline_style)) = &mut current_underline {
|
||||||
if style_run.underline.as_ref() != Some(underline_style) {
|
if style_run.underline.as_ref() != Some(underline_style) {
|
||||||
finished_underline = current_underline.take();
|
finished_underline = current_underline.take();
|
||||||
|
@ -149,16 +175,30 @@ fn paint_line(
|
||||||
color = style_run.color;
|
color = style_run.color;
|
||||||
} else {
|
} else {
|
||||||
run_end = layout.len;
|
run_end = layout.len;
|
||||||
|
finished_background = current_background.take();
|
||||||
finished_underline = current_underline.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 {
|
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_origin,
|
underline_origin,
|
||||||
glyph_origin.x - underline_origin.x,
|
glyph_origin.x - underline_origin.x,
|
||||||
&underline_style,
|
&underline_style,
|
||||||
)?;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_glyph_bounds = Bounds {
|
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() {
|
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);
|
let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
|
||||||
cx.paint_underline(
|
cx.paint_underline(
|
||||||
underline_start,
|
underline_start,
|
||||||
line_end_x - underline_start.x,
|
line_end_x - underline_start.x,
|
||||||
&underline_style,
|
&underline_style,
|
||||||
)?;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -198,6 +198,41 @@ impl WrappedLineLayout {
|
||||||
pub fn runs(&self) -> &[ShapedRun] {
|
pub fn runs(&self) -> &[ShapedRun] {
|
||||||
&self.unwrapped_layout.runs
|
&self.unwrapped_layout.runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn index_for_position(
|
||||||
|
&self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
) -> Option<usize> {
|
||||||
|
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 {
|
pub(crate) struct LineLayoutCache {
|
||||||
|
|
|
@ -881,7 +881,7 @@ impl<'a> WindowContext<'a> {
|
||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
width: Pixels,
|
width: Pixels,
|
||||||
style: &UnderlineStyle,
|
style: &UnderlineStyle,
|
||||||
) -> Result<()> {
|
) {
|
||||||
let scale_factor = self.scale_factor();
|
let scale_factor = self.scale_factor();
|
||||||
let height = if style.wavy {
|
let height = if style.wavy {
|
||||||
style.thickness * 3.
|
style.thickness * 3.
|
||||||
|
@ -905,7 +905,6 @@ impl<'a> WindowContext<'a> {
|
||||||
wavy: style.wavy,
|
wavy: style.wavy,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
|
/// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use gpui::{
|
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;
|
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. ",
|
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
|
||||||
"Meanwhile, the lazy dog decided it was time for a change. ",
|
"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.",
|
"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}");
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@ impl RenderOnce for HighlightedLabel {
|
||||||
LabelSize::Default => this.text_ui(),
|
LabelSize::Default => this.text_ui(),
|
||||||
LabelSize::Small => this.text_ui_sm(),
|
LabelSize::Small => this.text_ui_sm(),
|
||||||
})
|
})
|
||||||
.child(StyledText::new(self.label, runs))
|
.child(StyledText::new(self.label).with_runs(runs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue