ailloli_ui_editor/layout/layout_cache.rs
1//! Per-engine paragraph shaping cache and quantized cache keys.
2
3use std::collections::hash_map::DefaultHasher;
4use std::collections::HashMap;
5use std::hash::{Hash, Hasher};
6
7use ailloli_ui_core::TextStyle;
8use ailloli_ui_text::{TextLayoutHandle, TextLayoutParams, TextSystem, WrapMode};
9
10/// Internal identity for one shaped paragraph layout.
11///
12/// The key combines paragraph identity/revision, a full text hash and byte
13/// length, font and size, wrap mode, and a width quantized to 1/64 logical
14/// pixel. It deliberately omits paint-only color. Callers normally exercise it
15/// indirectly through [`crate::EditorEngine`].
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_editor::EditorEngine;
21/// let mut engine = EditorEngine::new();
22/// engine.clear_caches(); // clears layouts identified by internal cache keys
23/// ```
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub(crate) struct LayoutCacheKey {
26 /// Zero-based logical paragraph index.
27 pub paragraph_index: usize,
28 /// Caller-owned paragraph revision.
29 pub paragraph_revision: u64,
30 /// Process-local hash of the complete newline-trimmed paragraph text.
31 pub text_hash: u64,
32 /// UTF-8 byte length used as an additional collision discriminator.
33 pub text_len: usize,
34 /// Font family identifier participating in shaping.
35 pub font: ailloli_ui_core::FontId,
36 /// Font size in logical pixels.
37 pub px_size: u16,
38 /// Line-breaking policy.
39 pub wrap_mode: WrapMode,
40 /// Width in 1/64 logical-pixel units, or [`u32::MAX`] for no-wrap.
41 pub max_width_q: u32,
42}
43
44/// Constructs internal paragraph-layout keys.
45impl LayoutCacheKey {
46 /// Hashes and quantizes inputs into a cache key.
47 ///
48 /// Under wrapping, negative widths clamp to zero, `None` maps to zero, and
49 /// conversion to `u32` follows Rust's saturating float-to-integer cast. In
50 /// no-wrap mode the width is ignored and uses the [`u32::MAX`] sentinel.
51 ///
52 /// # Examples
53 ///
54 /// ```
55 /// use ailloli_ui_editor::EditorEngine;
56 /// let mut engine = EditorEngine::new();
57 /// engine.clear_caches(); // the engine rebuilds keys from later frame inputs
58 /// ```
59 pub(crate) fn new(
60 paragraph_index: usize,
61 paragraph_revision: u64,
62 text: &str,
63 style: TextStyle,
64 wrap_mode: WrapMode,
65 max_width: Option<f32>,
66 ) -> Self {
67 let mut hasher = DefaultHasher::new();
68 text.hash(&mut hasher);
69 let max_width_q = match wrap_mode {
70 WrapMode::NoWrap => u32::MAX,
71 WrapMode::Word | WrapMode::WordOrAnywhere => max_width
72 .map(|w| (w.max(0.0) * 64.0).round() as u32)
73 .unwrap_or(0),
74 };
75 Self {
76 paragraph_index,
77 paragraph_revision,
78 text_hash: hasher.finish(),
79 text_len: text.len(),
80 font: style.font,
81 px_size: style.px_size,
82 wrap_mode,
83 max_width_q,
84 }
85 }
86}
87
88/// Per-engine paragraph layout cache layered over `TextSystem`.
89///
90/// Entries have no independent eviction policy; call [`LayoutCache::clear`] at
91/// lifecycle boundaries or let the owning engine drop the cache. Stored handles
92/// are cheap [`std::sync::Arc`] clones of prepared layouts.
93///
94/// # Examples
95///
96/// ```
97/// use ailloli_ui_editor::layout::LayoutCache;
98/// let mut cache = LayoutCache::default();
99/// cache.clear();
100/// ```
101#[derive(Debug, Clone, Default)]
102pub struct LayoutCache {
103 /// Prepared layouts indexed by all geometry-affecting inputs.
104 layouts: HashMap<LayoutCacheKey, TextLayoutHandle>,
105}
106
107/// Resolves and invalidates cached paragraph layouts.
108impl LayoutCache {
109 /// Returns a cached layout or shapes and stores a new one.
110 ///
111 /// Internal callers must build `key` from the exact accompanying text,
112 /// style, wrap mode, and width; a reused but inconsistent key returns its
113 /// existing layout without comparing those arguments.
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use ailloli_ui_editor::EditorEngine;
119 /// let mut engine = EditorEngine::new();
120 /// engine.clear_caches(); // frame construction repopulates this internal cache
121 /// ```
122 pub(crate) fn layout_paragraph(
123 &mut self,
124 key: LayoutCacheKey,
125 text: &str,
126 style: TextStyle,
127 max_width: Option<f32>,
128 wrap_mode: WrapMode,
129 text_system: &mut TextSystem,
130 ) -> TextLayoutHandle {
131 if let Some(layout) = self.layouts.get(&key) {
132 return layout.clone();
133 }
134 let layout = text_system.layout_cached(TextLayoutParams {
135 text,
136 style,
137 max_width,
138 wrap_mode,
139 });
140 self.layouts.insert(key, layout.clone());
141 layout
142 }
143
144 /// Removes every prepared paragraph layout from this cache.
145 ///
146 /// This does not clear the separate cache owned by a [`TextSystem`].
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// use ailloli_ui_editor::layout::LayoutCache;
152 /// let mut cache = LayoutCache::default();
153 /// cache.clear();
154 /// ```
155 pub fn clear(&mut self) {
156 self.layouts.clear();
157 }
158}