Lots of claude-driven performance work.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Renderer-oriented scene snapshot types.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::ops::{Deref, Range};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cosmic_text::CacheKey;
|
||||
use tracing::debug;
|
||||
@@ -56,6 +57,13 @@ impl Rect {
|
||||
&& point.x < self.origin.x + self.size.width
|
||||
&& point.y < self.origin.y + self.size.height
|
||||
}
|
||||
|
||||
pub fn intersects(self, other: Rect) -> bool {
|
||||
self.origin.x < other.origin.x + other.size.width
|
||||
&& self.origin.x + self.size.width > other.origin.x
|
||||
&& self.origin.y < other.origin.y + other.size.height
|
||||
&& self.origin.y + self.size.height > other.origin.y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -67,6 +75,11 @@ pub struct Color {
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Sentinel value meaning "use the PreparedText default_color at render time".
|
||||
/// Fully transparent black (a == 0) is never a useful visible color, so it is
|
||||
/// safe to reserve as a sentinel. No user-facing API sets this value directly.
|
||||
pub const SENTINEL: Self = Self::rgba(0, 0, 0, 0);
|
||||
|
||||
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
@@ -74,6 +87,12 @@ impl Color {
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::rgba(r, g, b, 255)
|
||||
}
|
||||
|
||||
/// Returns `true` when this color is the sentinel "use default" marker.
|
||||
#[inline]
|
||||
pub const fn is_sentinel(self) -> bool {
|
||||
self.a == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -130,6 +149,10 @@ pub struct ClipRegion {
|
||||
pub struct GlyphInstance {
|
||||
pub position: Point,
|
||||
pub advance: f32,
|
||||
/// Glyph color. When equal to `Color::SENTINEL` the renderer uses the
|
||||
/// enclosing `PreparedText::default_color`. This allows the text layout
|
||||
/// cache to be color-independent: the same shaped data can be reused for
|
||||
/// the same text displayed in different colors.
|
||||
pub color: Color,
|
||||
pub cache_key: Option<CacheKey>,
|
||||
pub text_start: usize,
|
||||
@@ -145,6 +168,21 @@ pub struct PreparedTextLine {
|
||||
pub glyph_end: usize,
|
||||
}
|
||||
|
||||
/// Shaped, positioned text ready for rendering.
|
||||
///
|
||||
/// Glyphs and line rects are stored in **local (origin-relative) coordinates**:
|
||||
/// add `self.origin` to convert to absolute window coords. This keeps the
|
||||
/// `Arc<TextLayoutData>` from the text shape cache shareable across different
|
||||
/// on-screen positions — no per-frame translation is required.
|
||||
///
|
||||
/// The `lines` and `glyphs` are stored behind an `Arc` so that cloning a
|
||||
/// `PreparedText` (e.g. to put the same shaped text into both the scene
|
||||
/// snapshot and the interaction tree) is O(1). The `Arc` is shared as long
|
||||
/// as no mutation is needed; `apply_selected_text_color` uses
|
||||
/// `Arc::make_mut` and clones on first write.
|
||||
///
|
||||
/// Glyph colors may be `Color::SENTINEL` — the renderer substitutes
|
||||
/// `default_color` for those glyphs.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PreparedText {
|
||||
pub element_id: Option<ElementId>,
|
||||
@@ -153,22 +191,69 @@ pub struct PreparedText {
|
||||
pub bounds: Option<UiSize>,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub color: Color,
|
||||
/// Default color applied to sentinel-colored glyphs at render time.
|
||||
pub default_color: Color,
|
||||
pub selectable: bool,
|
||||
pub selection_style: TextSelectionStyle,
|
||||
pub lines: Vec<PreparedTextLine>,
|
||||
pub glyphs: Vec<GlyphInstance>,
|
||||
layout: Arc<TextLayoutData>,
|
||||
}
|
||||
|
||||
/// Shaped glyph and line data shared between scene and interaction-tree copies
|
||||
/// of the same `PreparedText`. Coordinates are LOCAL (relative to `PreparedText::origin`).
|
||||
/// Add `origin` to convert to absolute window coords.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PreparedImage {
|
||||
pub element_id: Option<ElementId>,
|
||||
pub resource: ImageResource,
|
||||
pub rect: Rect,
|
||||
pub uv_rect: (f32, f32, f32, f32),
|
||||
pub struct TextLayoutData {
|
||||
pub lines: Vec<PreparedTextLine>,
|
||||
pub glyphs: Vec<GlyphInstance>,
|
||||
/// Measured (unclamped) size of the laid-out text.
|
||||
pub size: UiSize,
|
||||
}
|
||||
|
||||
impl Deref for PreparedText {
|
||||
type Target = TextLayoutData;
|
||||
|
||||
fn deref(&self) -> &TextLayoutData {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
impl PreparedText {
|
||||
/// Construct a `PreparedText` from shaped data. Called by `TextSystem::prepare_spans`.
|
||||
pub(crate) fn from_layout(
|
||||
element_id: Option<ElementId>,
|
||||
text: String,
|
||||
origin: Point,
|
||||
bounds: Option<UiSize>,
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
default_color: Color,
|
||||
selectable: bool,
|
||||
selection_style: TextSelectionStyle,
|
||||
layout: Arc<TextLayoutData>,
|
||||
) -> Self {
|
||||
Self {
|
||||
element_id,
|
||||
text,
|
||||
origin,
|
||||
bounds,
|
||||
font_size,
|
||||
line_height,
|
||||
default_color,
|
||||
selectable,
|
||||
selection_style,
|
||||
layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a raw pointer to the underlying `TextLayoutData` allocation.
|
||||
/// Used in tests to verify Arc sharing between PreparedTexts.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn layout_ptr(&self) -> *const TextLayoutData {
|
||||
Arc::as_ptr(&self.layout)
|
||||
}
|
||||
|
||||
/// Create a monospace `PreparedText` without going through the text system.
|
||||
/// Used for testing and low-level terminal-style rendering.
|
||||
pub fn monospace(
|
||||
text: impl Into<String>,
|
||||
origin: Point,
|
||||
@@ -177,21 +262,31 @@ impl PreparedText {
|
||||
color: Color,
|
||||
) -> Self {
|
||||
let text = text.into();
|
||||
let mut x = origin.x;
|
||||
// Glyphs are stored in LOCAL (origin-relative) coordinates.
|
||||
let mut local_x = 0.0f32;
|
||||
let mut glyphs = Vec::with_capacity(text.chars().count());
|
||||
for (text_start, ch) in text.char_indices() {
|
||||
let text_end = text_start + ch.len_utf8();
|
||||
// monospace() builds glyphs directly; use the actual color (not sentinel)
|
||||
// because this path does not go through the text cache.
|
||||
glyphs.push(GlyphInstance {
|
||||
position: Point::new(x, origin.y),
|
||||
position: Point::new(local_x, 0.0),
|
||||
advance,
|
||||
color,
|
||||
cache_key: None,
|
||||
text_start,
|
||||
text_end,
|
||||
});
|
||||
x += advance;
|
||||
local_x += advance;
|
||||
}
|
||||
|
||||
let line = PreparedTextLine {
|
||||
rect: Rect::new(0.0, 0.0, local_x, font_size),
|
||||
text_start: 0,
|
||||
text_end: glyphs.last().map_or(0, |glyph| glyph.text_end),
|
||||
glyph_start: 0,
|
||||
glyph_end: glyphs.len(),
|
||||
};
|
||||
let size = UiSize::new(local_x, font_size);
|
||||
Self {
|
||||
element_id: None,
|
||||
text,
|
||||
@@ -199,25 +294,34 @@ impl PreparedText {
|
||||
bounds: None,
|
||||
font_size,
|
||||
line_height: font_size,
|
||||
color,
|
||||
default_color: 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,
|
||||
layout: Arc::new(TextLayoutData {
|
||||
lines: vec![line],
|
||||
glyphs,
|
||||
size,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate this text by `offset`. O(1): only `self.origin` is updated.
|
||||
/// Glyphs are stored in local coords and are unaffected.
|
||||
pub fn translated(mut self, offset: Point) -> Self {
|
||||
self.origin.x += offset.x;
|
||||
self.origin.y += offset.y;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the byte offset within `self.text` for an absolute window-space point.
|
||||
pub fn byte_offset_for_position(&self, point: Point) -> usize {
|
||||
let Some(line) = self.line_for_position(point.y) else {
|
||||
// Convert to local coords before delegating to the local-space helpers.
|
||||
let local_x = point.x - self.origin.x;
|
||||
let local_y = point.y - self.origin.y;
|
||||
let Some(line) = self.line_for_position(local_y) else {
|
||||
return 0;
|
||||
};
|
||||
self.byte_offset_for_line_position(line, point.x)
|
||||
self.byte_offset_for_line_position(line, local_x)
|
||||
}
|
||||
|
||||
pub fn selection_range(&self, start: usize, end: usize) -> Range<usize> {
|
||||
@@ -252,9 +356,10 @@ impl PreparedText {
|
||||
}
|
||||
|
||||
if let (Some(left), Some(right)) = (left, right) {
|
||||
// Glyph x/y and line rect are in local coords; add origin for absolute result.
|
||||
rects.push(Rect::new(
|
||||
left,
|
||||
line.rect.origin.y,
|
||||
self.origin.x + left,
|
||||
self.origin.y + line.rect.origin.y,
|
||||
(right - left).max(0.0),
|
||||
line.rect.size.height,
|
||||
));
|
||||
@@ -266,10 +371,11 @@ impl PreparedText {
|
||||
pub fn caret_rect(&self, offset: usize, width: f32) -> Option<Rect> {
|
||||
let width = width.max(0.0);
|
||||
let line = self.line_for_offset(offset)?;
|
||||
let x = self.caret_x_for_line_offset(line, offset);
|
||||
// caret_x_for_line_offset returns local x; add origin for absolute result.
|
||||
let local_x = self.caret_x_for_line_offset(line, offset);
|
||||
Some(Rect::new(
|
||||
x,
|
||||
line.rect.origin.y,
|
||||
self.origin.x + local_x,
|
||||
self.origin.y + line.rect.origin.y,
|
||||
width,
|
||||
line.rect.size.height,
|
||||
))
|
||||
@@ -346,6 +452,8 @@ impl PreparedText {
|
||||
start..end
|
||||
}
|
||||
|
||||
/// Apply `selection_style.text_color` to glyphs in `[start, end)`.
|
||||
/// Clones the inner `Arc<TextLayoutData>` on first call if it is shared.
|
||||
pub fn apply_selected_text_color(&mut self, start: usize, end: usize) {
|
||||
let Some(selected_color) = self.selection_style.text_color else {
|
||||
return;
|
||||
@@ -354,7 +462,7 @@ impl PreparedText {
|
||||
if range.is_empty() {
|
||||
return;
|
||||
}
|
||||
for glyph in &mut self.glyphs {
|
||||
for glyph in Arc::make_mut(&mut self.layout).glyphs.iter_mut() {
|
||||
if glyph.text_end > range.start && glyph.text_start < range.end {
|
||||
glyph.color = selected_color;
|
||||
}
|
||||
@@ -488,6 +596,14 @@ fn classify_word_char(ch: char) -> WordClass {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PreparedImage {
|
||||
pub element_id: Option<ElementId>,
|
||||
pub resource: ImageResource,
|
||||
pub rect: Rect,
|
||||
pub uv_rect: (f32, f32, f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DisplayItem {
|
||||
Quad(Quad),
|
||||
@@ -503,6 +619,42 @@ pub enum DisplayItem {
|
||||
LayerEnd,
|
||||
}
|
||||
|
||||
impl DisplayItem {
|
||||
/// Return a copy of this item with all positions shifted by `offset`.
|
||||
pub fn translated(&self, offset: Point) -> Self {
|
||||
fn translate_rect(r: Rect, o: Point) -> Rect {
|
||||
Rect::new(r.origin.x + o.x, r.origin.y + o.y, r.size.width, r.size.height)
|
||||
}
|
||||
match self {
|
||||
Self::Quad(q) => Self::Quad(Quad { rect: translate_rect(q.rect, offset), ..*q }),
|
||||
Self::RoundedRect(r) => Self::RoundedRect(RoundedRect {
|
||||
rect: translate_rect(r.rect, offset),
|
||||
..*r
|
||||
}),
|
||||
Self::ShadowRect(s) => Self::ShadowRect(ShadowRect {
|
||||
rect: translate_rect(s.rect, offset),
|
||||
source_rect: translate_rect(s.source_rect, offset),
|
||||
..*s
|
||||
}),
|
||||
Self::Image(img) => Self::Image(PreparedImage {
|
||||
rect: translate_rect(img.rect, offset),
|
||||
..img.clone()
|
||||
}),
|
||||
Self::Text(text) => Self::Text(text.clone().translated(offset)),
|
||||
Self::PushClip(clip) => Self::PushClip(ClipRegion {
|
||||
rect: translate_rect(clip.rect, offset),
|
||||
..*clip
|
||||
}),
|
||||
// These items carry no position data or are balanced markers.
|
||||
Self::PopClip => Self::PopClip,
|
||||
Self::PushTransform(t) => Self::PushTransform(*t),
|
||||
Self::PopTransform => Self::PopTransform,
|
||||
Self::LayerBegin { opacity } => Self::LayerBegin { opacity: *opacity },
|
||||
Self::LayerEnd => Self::LayerEnd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SceneSnapshot {
|
||||
pub version: SceneVersion,
|
||||
@@ -581,6 +733,23 @@ impl SceneSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hash implementations for f32-containing types.
|
||||
|
||||
impl std::hash::Hash for Point {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.x.to_bits().hash(state);
|
||||
self.y.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for UiSize {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.width.to_bits().hash(state);
|
||||
self.height.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Color, Point, PreparedText, Rect};
|
||||
@@ -669,6 +838,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prepared_text_vertical_offset_moves_between_lines() {
|
||||
use std::sync::Arc;
|
||||
use super::{PreparedTextLine, TextLayoutData};
|
||||
|
||||
let mut text = PreparedText::monospace(
|
||||
"abcdwxyz",
|
||||
Point::new(10.0, 20.0),
|
||||
@@ -676,28 +848,32 @@ mod tests {
|
||||
8.0,
|
||||
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||
);
|
||||
text.lines = vec![
|
||||
super::PreparedTextLine {
|
||||
rect: Rect::new(10.0, 20.0, 32.0, 16.0),
|
||||
// Lines and glyphs use LOCAL (origin-relative) coordinates.
|
||||
let lines = vec![
|
||||
PreparedTextLine {
|
||||
rect: Rect::new(0.0, 0.0, 32.0, 16.0),
|
||||
text_start: 0,
|
||||
text_end: 4,
|
||||
glyph_start: 0,
|
||||
glyph_end: 4,
|
||||
},
|
||||
super::PreparedTextLine {
|
||||
rect: Rect::new(10.0, 36.0, 32.0, 16.0),
|
||||
PreparedTextLine {
|
||||
rect: Rect::new(0.0, 16.0, 32.0, 16.0),
|
||||
text_start: 4,
|
||||
text_end: 8,
|
||||
glyph_start: 4,
|
||||
glyph_end: 8,
|
||||
},
|
||||
];
|
||||
for (index, glyph) in text.glyphs.iter_mut().enumerate() {
|
||||
let mut glyphs = text.glyphs.to_vec();
|
||||
for (index, glyph) in glyphs.iter_mut().enumerate() {
|
||||
if index >= 4 {
|
||||
glyph.position.y = 36.0;
|
||||
glyph.position.x = 10.0 + ((index - 4) as f32 * 8.0);
|
||||
glyph.position.y = 16.0;
|
||||
glyph.position.x = (index - 4) as f32 * 8.0;
|
||||
}
|
||||
}
|
||||
let orig_size = text.layout.size;
|
||||
text.layout = Arc::new(TextLayoutData { lines, glyphs, size: orig_size });
|
||||
|
||||
assert_eq!(text.vertical_offset(2, 1), Some(6));
|
||||
assert_eq!(text.vertical_offset(6, -1), Some(2));
|
||||
@@ -714,12 +890,14 @@ mod tests {
|
||||
assert_eq!(text.lines.len(), 3);
|
||||
|
||||
let target_line = &text.lines[1];
|
||||
let y = target_line.rect.origin.y + target_line.rect.size.height * 0.5;
|
||||
let start = text.byte_offset_for_position(Point::new(target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(target_line.rect.origin.x + 16.0, y));
|
||||
// Lines store LOCAL coords; add text.origin to get absolute window coords for the query.
|
||||
let y = text.origin.y + target_line.rect.origin.y + target_line.rect.size.height * 0.5;
|
||||
let start = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x + 16.0, y));
|
||||
let rects = text.selection_rects(start, end);
|
||||
|
||||
assert_eq!(rects.len(), 1);
|
||||
assert_eq!(rects[0].origin.y, target_line.rect.origin.y);
|
||||
// selection_rects returns absolute coords; compare against absolute line y.
|
||||
assert_eq!(rects[0].origin.y, text.origin.y + target_line.rect.origin.y);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user