mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
Allow finding which ranges were clicked on an InteractiveText
This commit is contained in:
parent
2cc1df9053
commit
56d043f671
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);}))
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user