
Follow-up to #24797 This PR ensures some cursor styles do not change for draggable elements during dragging. The linked PR covered this on the higher level for draggable divs. However, e.g. the pane divider inbetween two editors is not a draggable div and thus still has the issue that the cursor style changes during dragging. This PR fixes this issue by setting the hitbox to `None` in cases where the element is currently being dragged, which ensures the cursor style is applied to the cursor no matter what during dragging. Namely, this change fixes this for - non-div pane dividers - minimap slider and the - editor scrollbars and implements it for the UI scrollbars (Notably, UI scrollbars do already have `cursor_default` on their parent container but would not keep this during dragging. I opted out on removing this from the parent containers until #30194 or a similar PR is merged). https://github.com/user-attachments/assets/f97859dd-5f1d-4449-ab92-c27f2d933c4a Release Notes: - N/A
904 lines
31 KiB
Rust
904 lines
31 KiB
Rust
use crate::{
|
|
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
|
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
|
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
|
|
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
|
|
register_tooltip_mouse_handlers, set_tooltip_on_window,
|
|
};
|
|
use anyhow::Context as _;
|
|
use smallvec::SmallVec;
|
|
use std::{
|
|
cell::{Cell, RefCell},
|
|
mem,
|
|
ops::Range,
|
|
rc::Rc,
|
|
sync::Arc,
|
|
};
|
|
use util::ResultExt;
|
|
|
|
impl Element for &'static str {
|
|
type RequestLayoutState = TextLayout;
|
|
type PrepaintState = ();
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
None
|
|
}
|
|
|
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
let mut state = TextLayout::default();
|
|
let layout_id = state.layout(SharedString::from(*self), None, window, cx);
|
|
(layout_id, state)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
text_layout: &mut Self::RequestLayoutState,
|
|
_window: &mut Window,
|
|
_cx: &mut App,
|
|
) {
|
|
text_layout.prepaint(bounds, self)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
_bounds: Bounds<Pixels>,
|
|
text_layout: &mut TextLayout,
|
|
_: &mut (),
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
text_layout.paint(self, window, cx)
|
|
}
|
|
}
|
|
|
|
impl IntoElement for &'static str {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl IntoElement for String {
|
|
type Element = SharedString;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self.into()
|
|
}
|
|
}
|
|
|
|
impl Element for SharedString {
|
|
type RequestLayoutState = TextLayout;
|
|
type PrepaintState = ();
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
None
|
|
}
|
|
|
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
let mut state = TextLayout::default();
|
|
let layout_id = state.layout(self.clone(), None, window, cx);
|
|
(layout_id, state)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
text_layout: &mut Self::RequestLayoutState,
|
|
_window: &mut Window,
|
|
_cx: &mut App,
|
|
) {
|
|
text_layout.prepaint(bounds, self.as_ref())
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
_bounds: Bounds<Pixels>,
|
|
text_layout: &mut Self::RequestLayoutState,
|
|
_: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
text_layout.paint(self.as_ref(), window, cx)
|
|
}
|
|
}
|
|
|
|
impl IntoElement for SharedString {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
/// 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>>,
|
|
delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
|
layout: TextLayout,
|
|
}
|
|
|
|
impl StyledText {
|
|
/// Construct a new styled text element from the given string.
|
|
pub fn new(text: impl Into<SharedString>) -> Self {
|
|
StyledText {
|
|
text: text.into(),
|
|
runs: None,
|
|
delayed_highlights: None,
|
|
layout: TextLayout::default(),
|
|
}
|
|
}
|
|
|
|
/// Get the layout for this element. This can be used to map indices to pixels and vice versa.
|
|
pub fn layout(&self) -> &TextLayout {
|
|
&self.layout
|
|
}
|
|
|
|
/// Set the styling attributes for the given text, as well as
|
|
/// as any ranges of text that have had their style customized.
|
|
pub fn with_default_highlights(
|
|
mut self,
|
|
default_style: &TextStyle,
|
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
|
) -> Self {
|
|
debug_assert!(
|
|
self.delayed_highlights.is_none(),
|
|
"Can't use `with_default_highlights` and `with_highlights`"
|
|
);
|
|
let runs = Self::compute_runs(&self.text, default_style, highlights);
|
|
self.runs = Some(runs);
|
|
self
|
|
}
|
|
|
|
/// Set the styling attributes for the given text, as well as
|
|
/// as any ranges of text that have had their style customized.
|
|
pub fn with_highlights(
|
|
mut self,
|
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
|
) -> Self {
|
|
debug_assert!(
|
|
self.runs.is_none(),
|
|
"Can't use `with_highlights` and `with_default_highlights`"
|
|
);
|
|
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
|
|
self
|
|
}
|
|
|
|
fn compute_runs(
|
|
text: &str,
|
|
default_style: &TextStyle,
|
|
highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
|
|
) -> Vec<TextRun> {
|
|
let mut runs = Vec::new();
|
|
let mut ix = 0;
|
|
for (range, highlight) in highlights {
|
|
if ix < range.start {
|
|
runs.push(default_style.clone().to_run(range.start - ix));
|
|
}
|
|
runs.push(
|
|
default_style
|
|
.clone()
|
|
.highlight(highlight)
|
|
.to_run(range.len()),
|
|
);
|
|
ix = range.end;
|
|
}
|
|
if ix < text.len() {
|
|
runs.push(default_style.to_run(text.len() - ix));
|
|
}
|
|
runs
|
|
}
|
|
|
|
/// Set the text runs for this piece of text.
|
|
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
|
self.runs = Some(runs);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for StyledText {
|
|
type RequestLayoutState = ();
|
|
type PrepaintState = ();
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
None
|
|
}
|
|
|
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
let runs = self.runs.take().or_else(|| {
|
|
self.delayed_highlights.take().map(|delayed_highlights| {
|
|
Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
|
|
})
|
|
});
|
|
|
|
let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
|
|
(layout_id, ())
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_: &mut Self::RequestLayoutState,
|
|
_window: &mut Window,
|
|
_cx: &mut App,
|
|
) {
|
|
self.layout.prepaint(bounds, &self.text)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
_inspector_id: Option<&InspectorElementId>,
|
|
_bounds: Bounds<Pixels>,
|
|
_: &mut Self::RequestLayoutState,
|
|
_: &mut Self::PrepaintState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
self.layout.paint(&self.text, window, cx)
|
|
}
|
|
}
|
|
|
|
impl IntoElement for StyledText {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|
|
|
|
/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
|
|
#[derive(Default, Clone)]
|
|
pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
|
|
|
|
struct TextLayoutInner {
|
|
len: usize,
|
|
lines: SmallVec<[WrappedLine; 1]>,
|
|
line_height: Pixels,
|
|
wrap_width: Option<Pixels>,
|
|
size: Option<Size<Pixels>>,
|
|
bounds: Option<Bounds<Pixels>>,
|
|
}
|
|
|
|
impl TextLayout {
|
|
fn layout(
|
|
&self,
|
|
text: SharedString,
|
|
runs: Option<Vec<TextRun>>,
|
|
window: &mut Window,
|
|
_: &mut App,
|
|
) -> LayoutId {
|
|
let text_style = window.text_style();
|
|
let font_size = text_style.font_size.to_pixels(window.rem_size());
|
|
let line_height = text_style
|
|
.line_height
|
|
.to_pixels(font_size.into(), window.rem_size());
|
|
|
|
let mut runs = if let Some(runs) = runs {
|
|
runs
|
|
} else {
|
|
vec![text_style.to_run(text.len())]
|
|
};
|
|
|
|
let layout_id = window.request_measured_layout(Default::default(), {
|
|
let element_state = self.clone();
|
|
|
|
move |known_dimensions, available_space, window, cx| {
|
|
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
|
|
known_dimensions.width.or(match available_space.width {
|
|
crate::AvailableSpace::Definite(x) => Some(x),
|
|
_ => None,
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let (truncate_width, truncation_suffix) =
|
|
if let Some(text_overflow) = text_style.text_overflow.clone() {
|
|
let width = known_dimensions.width.or(match available_space.width {
|
|
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
|
|
Some(max_lines) => Some(x * max_lines),
|
|
None => Some(x),
|
|
},
|
|
_ => None,
|
|
});
|
|
|
|
match text_overflow {
|
|
TextOverflow::Truncate(s) => (width, s),
|
|
}
|
|
} else {
|
|
(None, "".into())
|
|
};
|
|
|
|
if let Some(text_layout) = element_state.0.borrow().as_ref() {
|
|
if text_layout.size.is_some()
|
|
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
|
{
|
|
return text_layout.size.unwrap();
|
|
}
|
|
}
|
|
|
|
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
|
let text = if let Some(truncate_width) = truncate_width {
|
|
line_wrapper.truncate_line(
|
|
text.clone(),
|
|
truncate_width,
|
|
&truncation_suffix,
|
|
&mut runs,
|
|
)
|
|
} else {
|
|
text.clone()
|
|
};
|
|
let len = text.len();
|
|
|
|
let Some(lines) = window
|
|
.text_system()
|
|
.shape_text(
|
|
text,
|
|
font_size,
|
|
&runs,
|
|
wrap_width, // Wrap if we know the width.
|
|
text_style.line_clamp, // Limit the number of lines if line_clamp is set.
|
|
)
|
|
.log_err()
|
|
else {
|
|
element_state.0.borrow_mut().replace(TextLayoutInner {
|
|
lines: Default::default(),
|
|
len: 0,
|
|
line_height,
|
|
wrap_width,
|
|
size: Some(Size::default()),
|
|
bounds: None,
|
|
});
|
|
return Size::default();
|
|
};
|
|
|
|
let mut size: Size<Pixels> = Size::default();
|
|
for line in &lines {
|
|
let line_size = line.size(line_height);
|
|
size.height += line_size.height;
|
|
size.width = size.width.max(line_size.width).ceil();
|
|
}
|
|
|
|
element_state.0.borrow_mut().replace(TextLayoutInner {
|
|
lines,
|
|
len,
|
|
line_height,
|
|
wrap_width,
|
|
size: Some(size),
|
|
bounds: None,
|
|
});
|
|
|
|
size
|
|
}
|
|
});
|
|
|
|
layout_id
|
|
}
|
|
|
|
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
|
|
let mut element_state = self.0.borrow_mut();
|
|
let element_state = element_state
|
|
.as_mut()
|
|
.with_context(|| format!("measurement has not been performed on {text}"))
|
|
.unwrap();
|
|
element_state.bounds = Some(bounds);
|
|
}
|
|
|
|
fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
|
|
let element_state = self.0.borrow();
|
|
let element_state = element_state
|
|
.as_ref()
|
|
.with_context(|| format!("measurement has not been performed on {text}"))
|
|
.unwrap();
|
|
let bounds = element_state
|
|
.bounds
|
|
.with_context(|| format!("prepaint has not been performed on {text}"))
|
|
.unwrap();
|
|
|
|
let line_height = element_state.line_height;
|
|
let mut line_origin = bounds.origin;
|
|
let text_style = window.text_style();
|
|
for line in &element_state.lines {
|
|
line.paint_background(
|
|
line_origin,
|
|
line_height,
|
|
text_style.text_align,
|
|
Some(bounds),
|
|
window,
|
|
cx,
|
|
)
|
|
.log_err();
|
|
line.paint(
|
|
line_origin,
|
|
line_height,
|
|
text_style.text_align,
|
|
Some(bounds),
|
|
window,
|
|
cx,
|
|
)
|
|
.log_err();
|
|
line_origin.y += line.size(line_height).height;
|
|
}
|
|
}
|
|
|
|
/// Get the byte index into the input of the pixel position.
|
|
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
|
|
let element_state = self.0.borrow();
|
|
let element_state = element_state
|
|
.as_ref()
|
|
.expect("measurement has not been performed");
|
|
let bounds = element_state
|
|
.bounds
|
|
.expect("prepaint has not been performed");
|
|
|
|
if position.y < bounds.top() {
|
|
return Err(0);
|
|
}
|
|
|
|
let line_height = element_state.line_height;
|
|
let mut line_origin = bounds.origin;
|
|
let mut line_start_ix = 0;
|
|
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;
|
|
line_start_ix += line.len() + 1;
|
|
} else {
|
|
let position_within_line = position - line_origin;
|
|
match line.index_for_position(position_within_line, line_height) {
|
|
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
|
|
Err(index_within_line) => return Err(line_start_ix + index_within_line),
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(line_start_ix.saturating_sub(1))
|
|
}
|
|
|
|
/// Get the pixel position for the given byte index.
|
|
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
|
|
let element_state = self.0.borrow();
|
|
let element_state = element_state
|
|
.as_ref()
|
|
.expect("measurement has not been performed");
|
|
let bounds = element_state
|
|
.bounds
|
|
.expect("prepaint has not been performed");
|
|
let line_height = element_state.line_height;
|
|
|
|
let mut line_origin = bounds.origin;
|
|
let mut line_start_ix = 0;
|
|
|
|
for line in &element_state.lines {
|
|
let line_end_ix = line_start_ix + line.len();
|
|
if index < line_start_ix {
|
|
break;
|
|
} else if index > line_end_ix {
|
|
line_origin.y += line.size(line_height).height;
|
|
line_start_ix = line_end_ix + 1;
|
|
continue;
|
|
} else {
|
|
let ix_within_line = index - line_start_ix;
|
|
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Retrieve the layout for the line containing the given byte index.
|
|
pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
|
|
let element_state = self.0.borrow();
|
|
let element_state = element_state
|
|
.as_ref()
|
|
.expect("measurement has not been performed");
|
|
let bounds = element_state
|
|
.bounds
|
|
.expect("prepaint has not been performed");
|
|
let line_height = element_state.line_height;
|
|
|
|
let mut line_origin = bounds.origin;
|
|
let mut line_start_ix = 0;
|
|
|
|
for line in &element_state.lines {
|
|
let line_end_ix = line_start_ix + line.len();
|
|
if index < line_start_ix {
|
|
break;
|
|
} else if index > line_end_ix {
|
|
line_origin.y += line.size(line_height).height;
|
|
line_start_ix = line_end_ix + 1;
|
|
continue;
|
|
} else {
|
|
return Some(line.layout.clone());
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// The bounds of this layout.
|
|
pub fn bounds(&self) -> Bounds<Pixels> {
|
|
self.0.borrow().as_ref().unwrap().bounds.unwrap()
|
|
}
|
|
|
|
/// The line height for this layout.
|
|
pub fn line_height(&self) -> Pixels {
|
|
self.0.borrow().as_ref().unwrap().line_height
|
|
}
|
|
|
|
/// The UTF-8 length of the underlying text.
|
|
pub fn len(&self) -> usize {
|
|
self.0.borrow().as_ref().unwrap().len
|
|
}
|
|
|
|
/// The text for this layout.
|
|
pub fn text(&self) -> String {
|
|
self.0
|
|
.borrow()
|
|
.as_ref()
|
|
.unwrap()
|
|
.lines
|
|
.iter()
|
|
.map(|s| s.text.to_string())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
/// The text for this layout (with soft-wraps as newlines)
|
|
pub fn wrapped_text(&self) -> String {
|
|
let mut lines = Vec::new();
|
|
for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
|
|
let mut seen = 0;
|
|
for boundary in wrapped.layout.wrap_boundaries.iter() {
|
|
let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
|
|
[boundary.glyph_ix]
|
|
.index;
|
|
|
|
lines.push(wrapped.text[seen..index].to_string());
|
|
seen = index;
|
|
}
|
|
lines.push(wrapped.text[seen..].to_string());
|
|
}
|
|
|
|
lines.join("\n")
|
|
}
|
|
}
|
|
|
|
/// A text element that can be interacted with.
|
|
pub struct InteractiveText {
|
|
element_id: ElementId,
|
|
text: StyledText,
|
|
click_listener:
|
|
Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
|
|
hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
|
|
tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
|
|
tooltip_id: Option<TooltipId>,
|
|
clickable_ranges: Vec<Range<usize>>,
|
|
}
|
|
|
|
struct InteractiveTextClickEvent {
|
|
mouse_down_index: usize,
|
|
mouse_up_index: usize,
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[derive(Default)]
|
|
pub struct InteractiveTextState {
|
|
mouse_down_index: Rc<Cell<Option<usize>>>,
|
|
hovered_index: Rc<Cell<Option<usize>>>,
|
|
active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
|
|
}
|
|
|
|
/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
|
|
impl InteractiveText {
|
|
/// Creates a new InteractiveText from the given text.
|
|
pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
|
|
Self {
|
|
element_id: id.into(),
|
|
text,
|
|
click_listener: None,
|
|
hover_listener: None,
|
|
tooltip_builder: None,
|
|
tooltip_id: None,
|
|
clickable_ranges: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// on_click is called when the user clicks on one of the given ranges, passing the index of
|
|
/// the clicked range.
|
|
pub fn on_click(
|
|
mut self,
|
|
ranges: Vec<Range<usize>>,
|
|
listener: impl Fn(usize, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.click_listener = Some(Box::new(move |ranges, event, window, 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, window, cx);
|
|
}
|
|
}
|
|
}));
|
|
self.clickable_ranges = ranges;
|
|
self
|
|
}
|
|
|
|
/// on_hover is called when the mouse moves over a character within the text, passing the
|
|
/// index of the hovered character, or None if the mouse leaves the text.
|
|
pub fn on_hover(
|
|
mut self,
|
|
listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.hover_listener = Some(Box::new(listener));
|
|
self
|
|
}
|
|
|
|
/// tooltip lets you specify a tooltip for a given character index in the string.
|
|
pub fn tooltip(
|
|
mut self,
|
|
builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
|
|
) -> Self {
|
|
self.tooltip_builder = Some(Rc::new(builder));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Element for InteractiveText {
|
|
type RequestLayoutState = ();
|
|
type PrepaintState = Hitbox;
|
|
|
|
fn id(&self) -> Option<ElementId> {
|
|
Some(self.element_id.clone())
|
|
}
|
|
|
|
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
|
None
|
|
}
|
|
|
|
fn request_layout(
|
|
&mut self,
|
|
_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> (LayoutId, Self::RequestLayoutState) {
|
|
self.text.request_layout(None, inspector_id, window, cx)
|
|
}
|
|
|
|
fn prepaint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
state: &mut Self::RequestLayoutState,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Hitbox {
|
|
window.with_optional_element_state::<InteractiveTextState, _>(
|
|
global_id,
|
|
|interactive_state, window| {
|
|
let mut interactive_state = interactive_state
|
|
.map(|interactive_state| interactive_state.unwrap_or_default());
|
|
|
|
if let Some(interactive_state) = interactive_state.as_mut() {
|
|
if self.tooltip_builder.is_some() {
|
|
self.tooltip_id =
|
|
set_tooltip_on_window(&interactive_state.active_tooltip, window);
|
|
} else {
|
|
// If there is no longer a tooltip builder, remove the active tooltip.
|
|
interactive_state.active_tooltip.take();
|
|
}
|
|
}
|
|
|
|
self.text
|
|
.prepaint(None, inspector_id, bounds, state, window, cx);
|
|
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
|
(hitbox, interactive_state)
|
|
},
|
|
)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
global_id: Option<&GlobalElementId>,
|
|
inspector_id: Option<&InspectorElementId>,
|
|
bounds: Bounds<Pixels>,
|
|
_: &mut Self::RequestLayoutState,
|
|
hitbox: &mut Hitbox,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
let current_view = window.current_view();
|
|
let text_layout = self.text.layout().clone();
|
|
window.with_element_state::<InteractiveTextState, _>(
|
|
global_id.unwrap(),
|
|
|interactive_state, window| {
|
|
let mut interactive_state = interactive_state.unwrap_or_default();
|
|
if let Some(click_listener) = self.click_listener.take() {
|
|
let mouse_position = window.mouse_position();
|
|
if let Ok(ix) = text_layout.index_for_position(mouse_position) {
|
|
if self
|
|
.clickable_ranges
|
|
.iter()
|
|
.any(|range| range.contains(&ix))
|
|
{
|
|
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
|
|
}
|
|
}
|
|
|
|
let text_layout = text_layout.clone();
|
|
let mouse_down = interactive_state.mouse_down_index.clone();
|
|
if let Some(mouse_down_index) = mouse_down.get() {
|
|
let hitbox = hitbox.clone();
|
|
let clickable_ranges = mem::take(&mut self.clickable_ranges);
|
|
window.on_mouse_event(
|
|
move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
if let Ok(mouse_up_index) =
|
|
text_layout.index_for_position(event.position)
|
|
{
|
|
click_listener(
|
|
&clickable_ranges,
|
|
InteractiveTextClickEvent {
|
|
mouse_down_index,
|
|
mouse_up_index,
|
|
},
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
|
|
mouse_down.take();
|
|
window.refresh();
|
|
}
|
|
},
|
|
);
|
|
} else {
|
|
let hitbox = hitbox.clone();
|
|
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
if let Ok(mouse_down_index) =
|
|
text_layout.index_for_position(event.position)
|
|
{
|
|
mouse_down.set(Some(mouse_down_index));
|
|
window.refresh();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
window.on_mouse_event({
|
|
let mut hover_listener = self.hover_listener.take();
|
|
let hitbox = hitbox.clone();
|
|
let text_layout = text_layout.clone();
|
|
let hovered_index = interactive_state.hovered_index.clone();
|
|
move |event: &MouseMoveEvent, phase, window, cx| {
|
|
if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
|
|
let current = hovered_index.get();
|
|
let updated = text_layout.index_for_position(event.position).ok();
|
|
if current != updated {
|
|
hovered_index.set(updated);
|
|
if let Some(hover_listener) = hover_listener.as_ref() {
|
|
hover_listener(updated, event.clone(), window, cx);
|
|
}
|
|
cx.notify(current_view);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if let Some(tooltip_builder) = self.tooltip_builder.clone() {
|
|
let active_tooltip = interactive_state.active_tooltip.clone();
|
|
let build_tooltip = Rc::new({
|
|
let tooltip_is_hoverable = false;
|
|
let text_layout = text_layout.clone();
|
|
move |window: &mut Window, cx: &mut App| {
|
|
text_layout
|
|
.index_for_position(window.mouse_position())
|
|
.ok()
|
|
.and_then(|position| tooltip_builder(position, window, cx))
|
|
.map(|view| (view, tooltip_is_hoverable))
|
|
}
|
|
});
|
|
|
|
// Use bounds instead of testing hitbox since this is called during prepaint.
|
|
let check_is_hovered_during_prepaint = Rc::new({
|
|
let source_bounds = hitbox.bounds;
|
|
let text_layout = text_layout.clone();
|
|
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
|
move |window: &Window| {
|
|
text_layout
|
|
.index_for_position(window.mouse_position())
|
|
.is_ok()
|
|
&& source_bounds.contains(&window.mouse_position())
|
|
&& pending_mouse_down.get().is_none()
|
|
}
|
|
});
|
|
|
|
let check_is_hovered = Rc::new({
|
|
let hitbox = hitbox.clone();
|
|
let text_layout = text_layout.clone();
|
|
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
|
move |window: &Window| {
|
|
text_layout
|
|
.index_for_position(window.mouse_position())
|
|
.is_ok()
|
|
&& hitbox.is_hovered(window)
|
|
&& pending_mouse_down.get().is_none()
|
|
}
|
|
});
|
|
|
|
register_tooltip_mouse_handlers(
|
|
&active_tooltip,
|
|
self.tooltip_id,
|
|
build_tooltip,
|
|
check_is_hovered,
|
|
check_is_hovered_during_prepaint,
|
|
window,
|
|
);
|
|
}
|
|
|
|
self.text
|
|
.paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
|
|
|
|
((), interactive_state)
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
impl IntoElement for InteractiveText {
|
|
type Element = Self;
|
|
|
|
fn into_element(self) -> Self::Element {
|
|
self
|
|
}
|
|
}
|