1use std::{
2 cell::{Cell, RefCell},
3 hash::{Hash, Hasher},
4 rc::Rc,
5};
6
7use cranpose_foundation::{
8 Constraints, DelegatableNode, DrawModifierNode, InvalidationKind, LayoutModifierNode,
9 Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement, NodeCapabilities,
10 NodeState, SemanticsConfiguration, SemanticsNode, Size,
11};
12
13use crate::text::{AnnotatedString, TextLayoutOptions, TextStyle};
14
15#[derive(Debug)]
25pub struct TextModifierNode {
26 layout: Rc<TextPreparedLayoutOwner>,
27 state: NodeState,
28}
29
30const PREPARED_LAYOUT_CACHE_CAPACITY: usize = 4;
31
32#[derive(Clone, Debug)]
33struct TextPreparedLayoutCacheEntry {
34 widths: crate::text::measure::PreparedWidths,
35 text_generation: u64,
36 font_scale_fingerprint: u32,
37 layout: Rc<crate::text::PreparedTextLayout>,
38}
39
40#[derive(Debug)]
41struct TextPreparedLayoutOwner {
42 text: Rc<AnnotatedString>,
43 style: TextStyle,
44 options: TextLayoutOptions,
45 node_id: Cell<Option<cranpose_core::NodeId>>,
46 measured_max_width: Cell<Option<Option<f32>>>,
47 cache: RefCell<Vec<TextPreparedLayoutCacheEntry>>,
48}
49
50#[derive(Clone, Debug)]
51pub(crate) struct TextPreparedLayoutHandle {
52 owner: Rc<TextPreparedLayoutOwner>,
53}
54
55impl TextPreparedLayoutOwner {
56 fn new(
57 text: Rc<AnnotatedString>,
58 style: TextStyle,
59 options: TextLayoutOptions,
60 node_id: Option<cranpose_core::NodeId>,
61 measured_max_width: Option<Option<f32>>,
62 ) -> Self {
63 Self {
64 text,
65 style,
66 options: options.normalized(),
67 node_id: Cell::new(node_id),
68 measured_max_width: Cell::new(measured_max_width),
69 cache: RefCell::new(Vec::new()),
70 }
71 }
72
73 fn text(&self) -> &str {
74 self.text.text.as_str()
75 }
76
77 fn annotated_text(&self) -> Rc<AnnotatedString> {
78 self.text.clone()
79 }
80
81 fn annotated_string(&self) -> AnnotatedString {
82 (*self.text).clone()
83 }
84
85 fn style(&self) -> &TextStyle {
86 &self.style
87 }
88
89 fn options(&self) -> TextLayoutOptions {
90 self.options
91 }
92
93 fn node_id(&self) -> Option<cranpose_core::NodeId> {
94 self.node_id.get()
95 }
96
97 fn set_node_id(&self, node_id: Option<cranpose_core::NodeId>) {
98 if self.node_id.replace(node_id) != node_id {
99 self.cache.borrow_mut().clear();
100 }
101 }
102
103 fn prepare(&self, max_width: Option<f32>) -> Rc<crate::text::PreparedTextLayout> {
107 let normalized_max_width = max_width.filter(|width| width.is_finite() && *width > 0.0);
108 let text_generation = crate::text::measure::current_text_generation();
109 let font_scale_fingerprint = crate::current_font_scale_curve().fingerprint();
110
111 {
112 let mut cache = self.cache.borrow_mut();
113 if let Some(index) = cache.iter().position(|entry| {
114 entry.widths.hold(normalized_max_width)
115 && entry.text_generation == text_generation
116 && entry.font_scale_fingerprint == font_scale_fingerprint
117 }) {
118 let prepared = Rc::clone(&cache[index].layout);
119 cache[..=index].rotate_right(1);
120 return prepared;
121 }
122 }
123
124 let prepared = crate::text::prepare_text_layout_for_node(
125 self.node_id(),
126 self.text.as_ref(),
127 &self.style,
128 self.options,
129 normalized_max_width,
130 );
131
132 let mut cache = self.cache.borrow_mut();
133 cache.insert(
134 0,
135 TextPreparedLayoutCacheEntry {
136 widths: crate::text::measure::PreparedWidths::of(
137 self.text.as_ref(),
138 self.options,
139 normalized_max_width,
140 &prepared,
141 ),
142 text_generation,
143 font_scale_fingerprint,
144 layout: Rc::clone(&prepared),
145 },
146 );
147 cache.truncate(PREPARED_LAYOUT_CACHE_CAPACITY);
148 prepared
149 }
150
151 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
152 let prepared = self.prepare(max_width);
153 Size {
154 width: prepared.metrics.width,
155 height: prepared.metrics.height,
156 }
157 }
158
159 fn measure_layout(&self, max_width: Option<f32>) -> Size {
160 self.measured_max_width.set(Some(max_width));
161 self.measure_text_content(max_width)
162 }
163
164 fn measured_layout(&self) -> Option<Rc<crate::text::PreparedTextLayout>> {
165 self.measured_max_width
166 .get()
167 .map(|max_width| self.prepare(max_width))
168 }
169}
170
171impl TextPreparedLayoutHandle {
172 fn new(owner: Rc<TextPreparedLayoutOwner>) -> Self {
173 Self { owner }
174 }
175
176 pub(crate) fn measured_layout(&self) -> Option<Rc<crate::text::PreparedTextLayout>> {
177 self.owner.measured_layout()
178 }
179}
180
181impl TextModifierNode {
182 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
183 Self {
184 layout: Rc::new(TextPreparedLayoutOwner::new(
185 text, style, options, None, None,
186 )),
187 state: NodeState::new(),
188 }
189 }
190
191 pub fn text(&self) -> &str {
192 self.layout.text()
193 }
194
195 pub fn annotated_text(&self) -> Rc<AnnotatedString> {
196 self.layout.annotated_text()
197 }
198
199 pub fn annotated_string(&self) -> AnnotatedString {
200 self.layout.annotated_string()
201 }
202
203 pub fn style(&self) -> &TextStyle {
204 self.layout.style()
205 }
206
207 pub fn options(&self) -> TextLayoutOptions {
208 self.layout.options()
209 }
210
211 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
212 self.layout.measure_text_content(max_width)
213 }
214
215 pub(crate) fn prepared_layout_handle(&self) -> TextPreparedLayoutHandle {
216 TextPreparedLayoutHandle::new(self.layout.clone())
217 }
218}
219
220impl DelegatableNode for TextModifierNode {
221 fn node_state(&self) -> &NodeState {
222 &self.state
223 }
224}
225
226impl ModifierNode for TextModifierNode {
227 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
228 self.layout.set_node_id(context.node_id());
229 context.invalidate(InvalidationKind::Layout);
230 context.invalidate(InvalidationKind::Draw);
231 context.invalidate(InvalidationKind::Semantics);
232 }
233
234 fn on_detach(&mut self) {
235 self.layout.set_node_id(None);
236 }
237
238 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
239 Some(self)
240 }
241
242 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
243 Some(self)
244 }
245
246 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
247 Some(self)
248 }
249
250 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
251 Some(self)
252 }
253
254 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
255 Some(self)
256 }
257
258 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
259 Some(self)
260 }
261}
262
263impl LayoutModifierNode for TextModifierNode {
264 fn measure(
265 &self,
266 _context: &mut dyn ModifierNodeContext,
267 _measurable: &dyn Measurable,
268 constraints: Constraints,
269 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
270 let max_width = constraints
271 .max_width
272 .is_finite()
273 .then_some(constraints.max_width);
274 let text_size = self.layout.measure_layout(max_width);
275
276 let width = text_size
277 .width
278 .clamp(constraints.min_width, constraints.max_width);
279 let height = text_size
280 .height
281 .clamp(constraints.min_height, constraints.max_height);
282
283 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
284 }
285
286 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
287 self.measure_text_content(None).width
288 }
289
290 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
291 self.measure_text_content(None).width
292 }
293
294 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
295 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
296 .height
297 }
298
299 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
300 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
301 .height
302 }
303}
304
305impl DrawModifierNode for TextModifierNode {}
306
307impl SemanticsNode for TextModifierNode {
308 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
309 config.content_description = Some(self.text().to_string());
310 }
311
312 fn reach(&self) -> cranpose_foundation::SemanticsReach {
313 cranpose_foundation::SemanticsReach::default()
314 }
315}
316
317#[derive(Debug, Clone, PartialEq)]
326pub struct TextModifierElement {
327 text: Rc<AnnotatedString>,
328 style: TextStyle,
329 options: TextLayoutOptions,
330}
331
332impl TextModifierElement {
333 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
334 Self {
335 text,
336 style,
337 options: options.normalized(),
338 }
339 }
340}
341
342impl Hash for TextModifierElement {
343 fn hash<H: Hasher>(&self, state: &mut H) {
344 self.text.render_hash().hash(state);
345 self.style.render_hash().hash(state);
346 self.options.hash(state);
347 }
348}
349
350impl ModifierNodeElement for TextModifierElement {
351 type Node = TextModifierNode;
352
353 fn create(&self) -> Self::Node {
354 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
355 }
356
357 fn update(&self, node: &mut Self::Node) {
358 let current = node.layout.as_ref();
359 if current.text != self.text
360 || current.style != self.style
361 || current.options != self.options
362 {
363 node.layout = Rc::new(TextPreparedLayoutOwner::new(
364 self.text.clone(),
365 self.style.clone(),
366 self.options,
367 current.node_id(),
368 current.measured_max_width.get(),
369 ));
370 }
371 }
372
373 fn capabilities(&self) -> NodeCapabilities {
374 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
375 }
376}
377
378#[cfg(test)]
379#[path = "tests/text_modifier_node_tests.rs"]
380mod tests;