More text improvements, performance enhancements, input handling, text selection, wl cursors
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user