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}