Allow finding which ranges were clicked on an InteractiveText

This commit is contained in:
Antonio Scandurra 2023-11-23 18:08:49 +01:00
parent 2cc1df9053
commit 56d043f671
4 changed files with 155 additions and 22 deletions

View File

@ -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<Vec<TextRun>>,
}
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<TextRun>) -> Self {
pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
text,
runs: Some(runs),
text: text.into(),
runs: None,
}
}
pub fn with_runs(mut self, runs: Vec<TextRun>) -> 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<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,
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,
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 {
@ -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<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)
}
}

View File

@ -198,6 +198,41 @@ impl WrappedLineLayout {
pub fn runs(&self) -> &[ShapedRun] {
&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 {

View File

@ -1,5 +1,6 @@
use gpui::{
blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext,
blue, div, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, View,
VisualContext, WindowContext,
};
use ui::v_stack;
@ -55,6 +56,6 @@ 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?")).on_click(vec![2..4], |event, cx| {dbg!(event);}))
}
}

View File

@ -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))
}
}