More text improvements, performance enhancements, input handling, text selection, wl cursors

This commit is contained in:
2026-03-20 22:24:29 -04:00
parent d79a3bb728
commit 423df4ae1f
15 changed files with 2458 additions and 265 deletions

View File

@@ -1,9 +1,13 @@
//! Renderer-oriented scene snapshot types.
use std::ops::Range;
use cosmic_text::CacheKey;
use tracing::debug;
use crate::text::TextSelectionStyle;
use crate::trace_targets;
use crate::tree::ElementId;
pub type SceneVersion = u64;
@@ -53,7 +57,7 @@ impl Rect {
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
@@ -97,21 +101,35 @@ impl Quad {
#[derive(Clone, Debug, PartialEq)]
pub struct GlyphInstance {
pub glyph: String,
pub position: Point,
pub advance: f32,
pub color: Color,
pub cache_key: Option<CacheKey>,
pub text_start: usize,
pub text_end: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreparedTextLine {
pub rect: Rect,
pub text_start: usize,
pub text_end: usize,
pub glyph_start: usize,
pub glyph_end: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreparedText {
pub element_id: Option<ElementId>,
pub text: String,
pub origin: Point,
pub bounds: Option<UiSize>,
pub font_size: f32,
pub line_height: f32,
pub color: Color,
pub selectable: bool,
pub selection_style: TextSelectionStyle,
pub lines: Vec<PreparedTextLine>,
pub glyphs: Vec<GlyphInstance>,
}
@@ -126,27 +144,147 @@ impl PreparedText {
let text = text.into();
let mut x = origin.x;
let mut glyphs = Vec::with_capacity(text.chars().count());
for ch in text.chars() {
for (text_start, ch) in text.char_indices() {
let text_end = text_start + ch.len_utf8();
glyphs.push(GlyphInstance {
glyph: ch.to_string(),
position: Point::new(x, origin.y),
advance,
color,
cache_key: None,
text_start,
text_end,
});
x += advance;
}
Self {
element_id: None,
text,
origin,
bounds: None,
font_size,
line_height: font_size,
color,
selectable: true,
selection_style: TextSelectionStyle::DEFAULT,
lines: vec![PreparedTextLine {
rect: Rect::new(origin.x, origin.y, x - origin.x, font_size),
text_start: 0,
text_end: glyphs.last().map_or(0, |glyph| glyph.text_end),
glyph_start: 0,
glyph_end: glyphs.len(),
}],
glyphs,
}
}
pub fn byte_offset_for_position(&self, point: Point) -> usize {
let Some(line) = self.line_for_position(point.y) else {
return 0;
};
self.byte_offset_for_line_position(line, point.x)
}
pub fn selection_range(&self, start: usize, end: usize) -> Range<usize> {
let start = start.min(self.text.len());
let end = end.min(self.text.len());
if start <= end { start..end } else { end..start }
}
pub fn selected_text(&self, start: usize, end: usize) -> Option<&str> {
let range = self.selection_range(start, end);
self.text.get(range)
}
pub fn selection_rects(&self, start: usize, end: usize) -> Vec<Rect> {
let range = self.selection_range(start, end);
if range.is_empty() {
return Vec::new();
}
let mut rects = Vec::new();
for line in &self.lines {
let mut left = None::<f32>;
let mut right = None::<f32>;
for glyph in &self.glyphs[line.glyph_start..line.glyph_end] {
if glyph.text_end <= range.start || glyph.text_start >= range.end {
continue;
}
let glyph_left = glyph.position.x;
let glyph_right = glyph.position.x + glyph.advance.max(0.0);
left = Some(left.map_or(glyph_left, |current| current.min(glyph_left)));
right = Some(right.map_or(glyph_right, |current| current.max(glyph_right)));
}
if let (Some(left), Some(right)) = (left, right) {
rects.push(Rect::new(
left,
line.rect.origin.y,
(right - left).max(0.0),
line.rect.size.height,
));
}
}
rects
}
pub fn apply_selected_text_color(&mut self, start: usize, end: usize) {
let Some(selected_color) = self.selection_style.text_color else {
return;
};
let range = self.selection_range(start, end);
if range.is_empty() {
return;
}
for glyph in &mut self.glyphs {
if glyph.text_end > range.start && glyph.text_start < range.end {
glyph.color = selected_color;
}
}
}
fn line_for_position(&self, y: f32) -> Option<&PreparedTextLine> {
let mut lines = self.lines.iter();
let first = lines.next()?;
if y < first.rect.origin.y {
return Some(first);
}
let mut last = first;
for line in std::iter::once(first).chain(lines) {
last = line;
if y < line.rect.origin.y + line.rect.size.height {
return Some(line);
}
}
Some(last)
}
fn byte_offset_for_line_position(&self, line: &PreparedTextLine, x: f32) -> usize {
if line.glyph_start == line.glyph_end {
return line.text_start;
}
let line_glyphs = &self.glyphs[line.glyph_start..line.glyph_end];
let first_glyph = &line_glyphs[0];
if x <= first_glyph.position.x {
return first_glyph.text_start;
}
for glyph in line_glyphs {
let glyph_left = glyph.position.x;
let glyph_right = glyph.position.x + glyph.advance.max(0.0);
let midpoint = glyph_left + glyph.advance.max(0.0) * 0.5;
if x < midpoint {
return glyph.text_start;
}
if x < glyph_right {
return glyph.text_end;
}
}
line.text_end
}
}
#[derive(Clone, Debug, PartialEq)]
@@ -226,3 +364,49 @@ impl SceneSnapshot {
self.push_item(DisplayItem::LayerEnd)
}
}
#[cfg(test)]
mod tests {
use super::{Color, Point, PreparedText, Rect};
use crate::TextSelectionStyle;
#[test]
fn prepared_text_hit_testing_clamps_to_nearest_cluster_boundary() {
let text = PreparedText::monospace(
"abcd",
Point::new(10.0, 20.0),
16.0,
8.0,
Color::rgb(0xFF, 0xFF, 0xFF),
);
assert_eq!(text.byte_offset_for_position(Point::new(6.0, 24.0)), 0);
assert_eq!(text.byte_offset_for_position(Point::new(13.0, 24.0)), 0);
assert_eq!(text.byte_offset_for_position(Point::new(17.0, 24.0)), 1);
assert_eq!(text.byte_offset_for_position(Point::new(33.0, 24.0)), 3);
assert_eq!(text.byte_offset_for_position(Point::new(50.0, 24.0)), 4);
}
#[test]
fn prepared_text_selection_rects_cover_selected_glyphs() {
let mut text = PreparedText::monospace(
"abcd",
Point::new(10.0, 20.0),
16.0,
8.0,
Color::rgb(0xFF, 0xFF, 0xFF),
);
text.selection_style = TextSelectionStyle::new(Color::rgba(0x44, 0x66, 0xFF, 0xAA))
.with_text_color(Color::rgb(0x11, 0x12, 0x1A));
assert_eq!(
text.selection_rects(1, 3),
vec![Rect::new(18.0, 20.0, 16.0, 16.0)]
);
assert_eq!(text.selected_text(1, 3), Some("bc"));
text.apply_selected_text_color(1, 3);
assert_eq!(text.glyphs[0].color, Color::rgb(0xFF, 0xFF, 0xFF));
assert_eq!(text.glyphs[1].color, Color::rgb(0x11, 0x12, 0x1A));
assert_eq!(text.glyphs[2].color, Color::rgb(0x11, 0x12, 0x1A));
}
}