use gpui::Corners;
use std::{
ops::Range,
rc::Rc,
sync::{Arc, Mutex},
};
use gpui::{
App, BorderStyle, Bounds, ClickEvent, CursorStyle, Edges, Element, ElementId, GlobalElementId,
Half, HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
SharedString, StyledText, TextLayout, TextRun, TextStyle, Window, point, px, quad,
};
use crate::{
GlobalState, TextSelection,
input::Selection,
text::TextViewMultiClickKind,
text::node::LinkMark,
text::selection::word_range_at,
text::state::LineSpan,
text::text_view::{LinkClickHandlerFn, handle_link_click},
};
#[derive(Clone, Debug, Default, PartialEq)]
pub(super) struct InlineHighlight {
pub(super) style: HighlightStyle,
pub(super) font_family: Option<SharedString>,
pub(super) font_size_scale: Option<f32>,
}
impl InlineHighlight {
fn highlight(mut self, other: &InlineHighlight) -> Self {
self.style = self.style.highlight(other.style);
if other.font_family.is_some() {
self.font_family = other.font_family.clone();
}
if other.font_size_scale.is_some() {
self.font_size_scale = other.font_size_scale;
}
self
}
}
impl From<HighlightStyle> for InlineHighlight {
fn from(style: HighlightStyle) -> Self {
Self {
style,
font_family: None,
font_size_scale: None,
}
}
}
pub(super) fn combine_highlights(
a: impl IntoIterator<Item = (Range<usize>, InlineHighlight)>,
b: impl IntoIterator<Item = (Range<usize>, InlineHighlight)>,
) -> Vec<(Range<usize>, InlineHighlight)> {
let mut endpoints = Vec::new();
let mut highlights = Vec::new();
for (range, highlight) in a.into_iter().chain(b) {
if !range.is_empty() {
let id = highlights.len();
endpoints.push((range.start, id, true));
endpoints.push((range.end, id, false));
highlights.push(highlight);
}
}
endpoints.sort_unstable_by_key(|(position, _, _)| *position);
let mut combined = Vec::new();
let mut active: Vec<usize> = Vec::new();
let mut ix = 0;
for (position, id, is_start) in endpoints {
if position > ix && !active.is_empty() {
let style = active.iter().fold(InlineHighlight::default(), |acc, id| {
acc.highlight(&highlights[*id])
});
combined.push((ix..position, style));
}
ix = position;
if is_start {
active.push(id);
} else {
active.retain(|active_id| *active_id != id);
}
}
combined
}
pub(super) fn text_runs(
text_len: usize,
default_style: &TextStyle,
highlights: &[(Range<usize>, InlineHighlight)],
) -> Vec<TextRun> {
let mut runs = Vec::with_capacity(highlights.len() * 2 + 1);
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
runs.push(default_style.clone().to_run(range.start - ix));
}
let mut run = default_style
.clone()
.highlight(highlight.style)
.to_run(range.len());
if let Some(family) = &highlight.font_family {
run.font.family = family.clone();
}
runs.push(run);
ix = range.end;
}
if ix < text_len {
runs.push(default_style.to_run(text_len - ix));
}
runs
}
pub(super) fn text_size_ranges(
text_len: usize,
highlights: &[(Range<usize>, InlineHighlight)],
) -> Vec<(Range<usize>, f32)> {
let mut ranges: Vec<(Range<usize>, f32)> = Vec::new();
let mut push = |range: Range<usize>, scale: f32| {
if range.is_empty() {
return;
}
if let Some((last, last_scale)) = ranges.last_mut()
&& *last_scale == scale
&& last.end == range.start
{
last.end = range.end;
} else {
ranges.push((range, scale));
}
};
let mut cursor = 0;
for (range, highlight) in highlights {
push(cursor..range.start, 1.);
push(range.clone(), highlight.font_size_scale.unwrap_or(1.));
cursor = range.end;
}
push(cursor..text_len, 1.);
ranges
}
pub(super) struct Inline {
id: ElementId,
text: SharedString,
links: Rc<Vec<(Range<usize>, LinkMark)>>,
highlights: Vec<(Range<usize>, InlineHighlight)>,
styled_text: StyledText,
paint_origin: Option<Point<Pixels>>,
selection_source: Option<(Arc<Mutex<InlineState>>, Range<usize>)>,
link_click_handler: Option<Arc<LinkClickHandlerFn>>,
state: Arc<Mutex<InlineState>>,
}
#[derive(Debug, Default, PartialEq)]
pub(crate) struct InlineState {
hovered_index: Option<usize>,
pub(super) text: SharedString,
pub(super) selection: Option<Selection>,
}
impl InlineState {
pub(crate) fn set_text(&mut self, text: SharedString) {
self.text = text;
}
}
impl Inline {
pub(super) fn new(
id: impl Into<ElementId>,
state: Arc<Mutex<InlineState>>,
links: Vec<(Range<usize>, LinkMark)>,
highlights: Vec<(Range<usize>, InlineHighlight)>,
link_click_handler: Option<Arc<LinkClickHandlerFn>>,
) -> Self {
let text = state
.lock()
.map(|state| state.text.clone())
.unwrap_or_default();
Self {
id: id.into(),
links: Rc::new(links),
highlights,
text: text.clone(),
styled_text: StyledText::new(text),
paint_origin: None,
selection_source: None,
link_click_handler,
state,
}
}
pub(super) fn paint_origin(mut self, origin: Point<Pixels>) -> Self {
self.paint_origin = Some(origin);
self
}
pub(super) fn selection_source(
mut self,
state: Arc<Mutex<InlineState>>,
range: Range<usize>,
) -> Self {
self.selection_source = Some((state, range));
self
}
fn link_for_position(
layout: &TextLayout,
links: &Vec<(Range<usize>, LinkMark)>,
position: Point<Pixels>,
) -> Option<LinkMark> {
let offset = layout.index_for_position(position).ok()?;
for (range, link) in links.iter() {
if range.contains(&offset) {
return Some(link.clone());
}
}
None
}
#[allow(unused)]
fn paint_selected_bounds(&self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
window.paint_quad(gpui::PaintQuad {
bounds,
background: gpui::hsla(0.58, 0.85, 0.62, 0.01).into(),
corner_radii: Corners::default(),
border_color: gpui::transparent_black(),
border_style: BorderStyle::default(),
border_widths: gpui::Edges::all(px(0.)),
});
}
fn layout_selections(
&self,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
window: &mut Window,
cx: &mut App,
) -> (bool, bool, Option<Selection>) {
let Some(text_view_state) = GlobalState::global(cx).text_view_state() else {
return (false, false, None);
};
let text_view_state = text_view_state.read(cx);
let is_selectable = text_view_state.is_selectable();
if !is_selectable {
return (false, false, None);
}
if text_view_state.is_all_selected() {
return (is_selectable, true, Some((0..self.text.len()).into()));
}
if let Some(selection) = text_view_state.multi_click_selection() {
return (
is_selectable,
true,
selection_for_multi_click(
&self.text,
text_layout,
*bounds,
selection.pos,
selection.kind,
)
.map(Selection::from),
);
}
let Some((selection_start, selection_end)) = text_view_state.selection_points(cx) else {
return (is_selectable, false, None);
};
let line_height = window.line_height();
let mut selection: Option<Selection> = None;
let mut offset = 0;
let mut chars = self.text.chars().peekable();
while let Some(c) = chars.next() {
let Some(pos) = text_layout.position_for_index(offset) else {
offset += c.len_utf8();
continue;
};
let next_offset = offset + c.len_utf8();
let mut char_width = line_height.half();
if let Some(next_pos) = text_layout.position_for_index(next_offset) {
if next_pos.y == pos.y {
char_width = next_pos.x - pos.x;
}
}
if point_in_text_selection(pos, char_width, selection_start, selection_end, line_height)
{
if selection.is_none() {
selection = Some((offset..offset).into());
}
if let Some(selection) = selection.as_mut() {
selection.end = next_offset;
}
}
offset = next_offset;
}
(true, true, selection)
}
fn text_line_bounds(
&self,
text_layout: &TextLayout,
line_height: Pixels,
mask_bounds: Bounds<Pixels>,
) -> Vec<Bounds<Pixels>> {
let mut line_bounds = Vec::new();
let mut current_line_y = None;
let mut current_bounds: Option<Bounds<Pixels>> = None;
let mut offset = 0;
for c in self.text.chars() {
let next_offset = offset + c.len_utf8();
let Some(pos) = text_layout.position_for_index(offset) else {
offset = next_offset;
continue;
};
let mut char_width = line_height.half();
if let Some(next_pos) = text_layout.position_for_index(next_offset) {
if next_pos.y == pos.y {
char_width = next_pos.x - pos.x;
}
}
let bounds = Bounds::from_corners(pos, point(pos.x + char_width, pos.y + line_height))
.intersect(&mask_bounds);
if bounds.size.width > px(0.) && bounds.size.height > px(0.) {
if current_line_y == Some(pos.y) {
if let Some(current) = current_bounds.as_mut() {
*current = current.union(&bounds);
}
} else {
if let Some(current) = current_bounds.take() {
line_bounds.push(current);
}
current_line_y = Some(pos.y);
current_bounds = Some(bounds);
}
}
offset = next_offset;
}
if let Some(current) = current_bounds {
line_bounds.push(current);
}
line_bounds
}
fn paint_selection(
selection: &Selection,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
window: &mut Window,
color: gpui::Hsla,
) {
let mut start = selection.start;
let mut end = selection.end;
if end < start {
std::mem::swap(&mut start, &mut end);
}
let Some(start_position) = text_layout.position_for_index(start) else {
return;
};
let Some(end_position) = text_layout.position_for_index(end) else {
return;
};
let line_height = text_layout.line_height();
if start_position.y == end_position.y {
window.paint_quad(quad(
Bounds::from_corners(
start_position,
point(end_position.x, end_position.y + line_height),
),
px(0.),
color,
Edges::default(),
gpui::transparent_black(),
BorderStyle::default(),
));
} else {
window.paint_quad(quad(
Bounds::from_corners(
start_position,
point(bounds.right(), start_position.y + line_height),
),
px(0.),
color,
Edges::default(),
gpui::transparent_black(),
BorderStyle::default(),
));
if end_position.y > start_position.y + line_height {
window.paint_quad(quad(
Bounds::from_corners(
point(bounds.left(), start_position.y + line_height),
point(bounds.right(), end_position.y),
),
px(0.),
color,
Edges::default(),
gpui::transparent_black(),
BorderStyle::default(),
));
}
window.paint_quad(quad(
Bounds::from_corners(
point(bounds.left(), end_position.y),
point(end_position.x, end_position.y + line_height),
),
px(0.),
color,
Edges::default(),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
}
impl IntoElement for Inline {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Inline {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_element_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let text_style = window.text_style();
let runs = text_runs(self.text.len(), &text_style, &self.highlights);
self.styled_text = StyledText::new(self.text.clone()).with_runs(runs);
let (layout_id, _) =
self.styled_text
.request_layout(global_element_id, inspector_id, 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::PrepaintState {
let bounds = Bounds::new(self.paint_origin.unwrap_or(bounds.origin), bounds.size);
self.styled_text
.prepaint(id, inspector_id, bounds, &mut (), window, cx);
if let Some(text_view_state) = GlobalState::global(cx).text_view_state().cloned() {
let state = text_view_state.read(cx);
if state.max_lines.is_some()
&& let Ok(mut line_spans) = state.line_spans.lock()
{
line_spans.push(LineSpan {
top: bounds.top(),
bottom: bounds.bottom(),
line_height: window.line_height(),
});
}
}
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
hitbox
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let bounds = Bounds::new(self.paint_origin.unwrap_or(bounds.origin), bounds.size);
let current_view = window.current_view();
let hitbox = prepaint;
let Ok(mut state) = self.state.lock() else {
return;
};
let text_layout = self.styled_text.layout().clone();
self.styled_text
.paint(global_id, None, bounds, &mut (), &mut (), window, cx);
let (is_selectable, is_selection, selection) =
self.layout_selections(&text_layout, &bounds, window, cx);
state.selection = selection;
if let Some((source, range)) = &self.selection_source
&& let Some(selection) = selection
&& let Ok(mut source) = source.lock()
{
let start = range.start + selection.start;
let end = range.start + selection.end;
source.selection = Some(match source.selection {
Some(previous) => Selection::new(previous.start.min(start), previous.end.max(end)),
None => Selection::new(start, end),
});
}
if is_selection || is_selectable {
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
}
let mouse_position = window.mouse_position();
if let Some(_) = Self::link_for_position(&text_layout, &self.links, mouse_position) {
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
}
if let Some(selection) = &state.selection {
let color = GlobalState::global(cx)
.text_view_state()
.map(|state| state.read(cx).text_view_style.selection())
.unwrap_or_else(|| crate::Theme::global(cx).tokens.colors.selection);
Self::paint_selection(selection, &text_layout, &bounds, window, color);
}
if is_selectable {
if let Some(text_view_state) = GlobalState::global(cx).text_view_state().cloned() {
let text_bounds = self.text_line_bounds(
&text_layout,
text_layout.line_height(),
window.content_mask().bounds,
);
text_view_state.update(cx, |state, _| {
state.selection_adapter.register_inline(text_bounds);
});
}
window.on_mouse_event({
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let inline_state = self.state.clone();
let text = self.text.clone();
let text_view_state = GlobalState::global(cx).text_view_state().cloned();
move |event: &MouseDownEvent, phase, window, cx| {
if !phase.bubble()
|| !hitbox.is_hovered(window)
|| event.button != MouseButton::Left
{
return;
}
let kind = match event.click_count {
2 => TextViewMultiClickKind::Word,
3 => TextViewMultiClickKind::Paragraph,
_ => return,
};
let Some(range) = selection_for_multi_click(
&text,
&text_layout,
hitbox.bounds,
event.position,
kind,
) else {
return;
};
let selected_text = text[range.clone()].to_string();
GlobalState::suppress_text_selection(cx);
if let Ok(mut inline_state) = inline_state.lock() {
inline_state.selection = Some(range.into());
}
if let Some(text_view_state) = &text_view_state {
text_view_state.update(cx, |state, cx| {
state.set_multi_click_selection(
event.position,
kind,
selected_text,
cx,
);
});
}
cx.notify(current_view);
}
});
}
window.on_mouse_event({
let hitbox = hitbox.clone();
let text_layout = text_layout.clone();
let mut hovered_index = state.hovered_index;
move |event: &MouseMoveEvent, phase, window, cx| {
if !phase.bubble() || !hitbox.is_hovered(window) {
return;
}
let current = hovered_index;
let updated = text_layout.index_for_position(event.position).ok();
if current != updated {
hovered_index = updated;
cx.notify(current_view);
}
}
});
if !is_selection {
window.on_mouse_event({
let links = self.links.clone();
let text_layout = text_layout.clone();
let hitbox = hitbox.clone();
let text_view_state = GlobalState::global(cx).text_view_state().cloned();
let link_click_handler = self.link_click_handler.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if !phase.bubble() || !hitbox.is_hovered(window) {
return;
}
if text_view_state
.as_ref()
.is_some_and(|state| state.read(cx).has_selection(cx))
{
return;
}
if let Some(link) =
Self::link_for_position(&text_layout, &links, event.position)
{
TextSelection::end(window, cx);
cx.stop_propagation();
let click = ClickEvent::Mouse(MouseClickEvent {
down: MouseDownEvent {
button: event.button,
position: event.position,
modifiers: event.modifiers,
click_count: event.click_count,
first_mouse: false,
},
up: event.clone(),
});
handle_link_click(&link_click_handler, link.url, click, window, cx);
}
}
});
}
}
}
fn selection_for_multi_click(
text: &str,
text_layout: &TextLayout,
bounds: Bounds<Pixels>,
pos: Point<Pixels>,
kind: TextViewMultiClickKind,
) -> Option<std::ops::Range<usize>> {
if !bounds.contains(&pos) {
return None;
}
let offset = text_layout.index_for_position(pos).ok()?;
match kind {
TextViewMultiClickKind::Word => word_range_at(text, offset),
TextViewMultiClickKind::Paragraph => (!text.is_empty()).then_some(0..text.len()),
}
}
fn point_in_text_selection(
pos: Point<Pixels>,
char_width: Pixels,
selection_start: Point<Pixels>,
selection_end: Point<Pixels>,
line_height: Pixels,
) -> bool {
let point_in_line = |point: Point<Pixels>| point.y >= pos.y && point.y < pos.y + line_height;
let top = selection_start.y.min(selection_end.y);
let bottom = selection_start.y.max(selection_end.y);
let x = pos.x + char_width.half();
if pos.y + line_height <= top || pos.y > bottom {
return false;
}
if point_in_line(selection_start) && point_in_line(selection_end) {
let left = selection_start.x.min(selection_end.x);
let right = selection_start.x.max(selection_end.x);
return x >= left && x <= right;
}
let (top_point, bottom_point) = if selection_start.y < selection_end.y {
(selection_start, selection_end)
} else {
(selection_end, selection_start)
};
let is_top_line = point_in_line(top_point);
let is_bottom_line = point_in_line(bottom_point);
if is_top_line {
return x >= top_point.x;
} else if is_bottom_line {
return x <= bottom_point.x;
} else {
return true;
}
}
#[cfg(test)]
pub(super) mod test_fonts {
use gpui::{
Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels,
PlatformTextSystem, RenderGlyphParams, ShapedGlyph, ShapedRun, Size, TextRenderingMode,
point, px, size,
};
use std::borrow::Cow;
pub(crate) const BODY: &str = "Body";
pub(crate) const MONO: &str = "Mono";
const BODY_ID: FontId = FontId(1);
const MONO_ID: FontId = FontId(2);
const UNITS_PER_EM: f32 = 1000.;
pub(crate) struct WideMonoTextSystem;
impl WideMonoTextSystem {
fn advance_units(font_id: FontId) -> f32 {
if font_id == MONO_ID { 1000. } else { 500. }
}
pub(crate) fn width_of(text: &str, family: &str, font_size: Pixels) -> Pixels {
let font_id = if family == MONO { MONO_ID } else { BODY_ID };
font_size * (Self::advance_units(font_id) / UNITS_PER_EM) * text.chars().count() as f32
}
}
impl PlatformTextSystem for WideMonoTextSystem {
fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> anyhow::Result<()> {
Ok(())
}
fn all_font_names(&self) -> Vec<String> {
vec![BODY.into(), MONO.into()]
}
fn font_id(&self, descriptor: &Font) -> anyhow::Result<FontId> {
Ok(if descriptor.family.as_ref() == MONO {
MONO_ID
} else {
BODY_ID
})
}
fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
FontMetrics {
units_per_em: UNITS_PER_EM as u32,
ascent: 800.,
descent: -200.,
line_gap: 0.,
underline_position: -100.,
underline_thickness: 50.,
cap_height: 700.,
x_height: 500.,
bounding_box: Bounds {
origin: point(0., -200.),
size: size(1000., 1000.),
},
}
}
fn typographic_bounds(
&self,
font_id: FontId,
_glyph_id: GlyphId,
) -> anyhow::Result<Bounds<f32>> {
Ok(Bounds {
origin: point(0., 0.),
size: size(Self::advance_units(font_id), 700.),
})
}
fn advance(&self, font_id: FontId, _glyph_id: GlyphId) -> anyhow::Result<Size<f32>> {
Ok(size(Self::advance_units(font_id), 0.))
}
fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
Some(GlyphId(ch as u32))
}
fn glyph_raster_bounds(
&self,
_params: &RenderGlyphParams,
) -> anyhow::Result<Bounds<DevicePixels>> {
Ok(Bounds::default())
}
fn rasterize_glyph(
&self,
_params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> anyhow::Result<(Size<DevicePixels>, Vec<u8>)> {
Ok((raster_bounds.size, Vec::new()))
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
let mut position = px(0.);
let mut shaped_runs = Vec::new();
let mut run_start = 0;
for run in runs {
let run_text = &text[run_start..run_start + run.len];
let advance = font_size * (Self::advance_units(run.font_id) / UNITS_PER_EM);
let mut glyphs = Vec::new();
for (ix, ch) in run_text.char_indices() {
glyphs.push(ShapedGlyph {
id: GlyphId(ch as u32),
position: point(position, px(0.)),
index: run_start + ix,
is_emoji: false,
});
position += advance;
}
shaped_runs.push(ShapedRun {
font_id: run.font_id,
glyphs,
});
run_start += run.len;
}
let metrics = self.font_metrics(BODY_ID);
LineLayout {
font_size,
width: position,
ascent: font_size * (metrics.ascent / UNITS_PER_EM),
descent: font_size * (metrics.descent / UNITS_PER_EM),
runs: shaped_runs,
len: text.len(),
}
}
fn recommended_rendering_mode(
&self,
_font_id: FontId,
_font_size: Pixels,
) -> TextRenderingMode {
TextRenderingMode::Grayscale
}
}
}
#[cfg(test)]
mod tests {
use super::{InlineHighlight, combine_highlights, point_in_text_selection, text_runs};
use gpui::{FontWeight, HighlightStyle, SharedString, TextStyle, point, px};
fn mono(style: HighlightStyle) -> InlineHighlight {
InlineHighlight {
style,
font_family: Some(SharedString::from("Mono")),
font_size_scale: None,
}
}
#[test]
fn text_runs_shape_a_code_highlight_in_its_font_family() {
let style = TextStyle {
font_family: SharedString::from("Body"),
..Default::default()
};
let highlights = vec![(4..8, mono(HighlightStyle::default()))];
let runs = text_runs(12, &style, &highlights);
let families = runs
.iter()
.map(|run| (run.len, run.font.family.as_ref()))
.collect::<Vec<_>>();
assert_eq!(families, vec![(4, "Body"), (4, "Mono"), (4, "Body")]);
}
#[test]
fn combine_highlights_cuts_a_bold_span_at_the_code_boundary() {
let bold = InlineHighlight::from(HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
});
let combined = combine_highlights(
vec![(0..10, bold)],
vec![(6..10, mono(HighlightStyle::default()))],
);
assert_eq!(combined.len(), 2);
assert_eq!(combined[0].0, 0..6);
assert_eq!(combined[0].1.style.font_weight, Some(FontWeight::BOLD));
assert_eq!(combined[0].1.font_family, None);
assert_eq!(combined[1].0, 6..10);
assert_eq!(combined[1].1.style.font_weight, Some(FontWeight::BOLD));
assert_eq!(combined[1].1.font_family.as_deref(), Some("Mono"));
}
#[test]
fn test_point_in_text_selection() {
let line_height = px(20.);
let char_width = px(10.);
let start = point(px(50.), px(50.));
let end = point(px(150.), px(150.));
assert!(point_in_text_selection(
point(px(50.), px(40.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(50.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(40.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(160.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(100.), px(70.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(40.), px(70.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(160.), px(70.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(100.), px(140.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(40.), px(140.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(160.), px(140.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(100.), px(20.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(100.), px(160.)),
char_width,
start,
end,
line_height
));
}
#[test]
fn test_point_in_text_selection_reversed_drag_direction() {
let line_height = px(20.);
let char_width = px(10.);
let start = point(px(80.), px(150.));
let end = point(px(150.), px(50.));
assert!(!point_in_text_selection(
point(px(140.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(150.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(75.), px(140.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(80.), px(140.)),
char_width,
start,
end,
line_height
));
}
#[test]
fn test_point_in_text_selection_same_visual_line_with_different_y() {
let line_height = px(20.);
let char_width = px(10.);
let start = point(px(100.), px(55.));
let end = point(px(60.), px(58.));
assert!(!point_in_text_selection(
point(px(40.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(70.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(110.), px(50.)),
char_width,
start,
end,
line_height
));
}
#[test]
fn test_point_in_text_selection_same_visual_line_with_reversed_y() {
let line_height = px(20.);
let char_width = px(10.);
let start = point(px(60.), px(58.));
let end = point(px(100.), px(55.));
assert!(!point_in_text_selection(
point(px(40.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(point_in_text_selection(
point(px(70.), px(50.)),
char_width,
start,
end,
line_height
));
assert!(!point_in_text_selection(
point(px(110.), px(50.)),
char_width,
start,
end,
line_height
));
}
}