Skip to main content

cranpose_ui/
text_modifier_node.rs

1//! Text modifier node implementation following Jetpack Compose's TextStringSimpleNode architecture.
2//!
3//! This module implements text content as a modifier node rather than as a measure policy,
4//! matching the Jetpack Compose pattern where text is treated as visual content (like background)
5//! rather than as a layout strategy.
6//!
7//! # Architecture
8//!
9//! In Jetpack Compose, `BasicText` uses:
10//! ```kotlin
11//! Layout(modifier.then(TextStringSimpleElement(...)), EmptyMeasurePolicy)
12//! ```
13//!
14//! Where `TextStringSimpleNode` implements:
15//! - `LayoutModifierNode` - handles text measurement
16//! - `DrawModifierNode` - handles text drawing
17//! - `SemanticsModifierNode` - provides text content for accessibility
18//!
19//! This follows the principle that `MeasurePolicy` is for child layout, while modifier nodes
20//! handle content rendering and measurement.
21
22use cranpose_foundation::{
23    Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
24    LayoutModifierNode, Measurable, MeasurementProxy, ModifierNode, ModifierNodeContext,
25    ModifierNodeElement, NodeCapabilities, NodeState, SemanticsConfiguration, SemanticsNode, Size,
26};
27use std::hash::{Hash, Hasher};
28use std::rc::Rc;
29
30/// Node that stores text content and handles measurement, drawing, and semantics.
31///
32/// This node implements three capabilities:
33/// - **Layout**: Measures text and returns appropriate size
34/// - **Draw**: Renders the text (placeholder for now)
35/// - **Semantics**: Provides text content for accessibility
36///
37/// Matches Jetpack Compose: `TextStringSimpleNode` in
38/// `compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt`
39#[derive(Debug)]
40pub struct TextModifierNode {
41    text: Rc<str>,
42    state: NodeState,
43}
44
45impl TextModifierNode {
46    pub fn new(text: Rc<str>) -> Self {
47        Self {
48            text,
49            state: NodeState::new(),
50        }
51    }
52
53    pub fn text(&self) -> &str {
54        &self.text
55    }
56
57    pub fn text_arc(&self) -> Rc<str> {
58        self.text.clone()
59    }
60
61    /// Helper to measure text content size.
62    fn measure_text_content(&self) -> Size {
63        let metrics = crate::text::measure_text(&self.text);
64        Size {
65            width: metrics.width,
66            height: metrics.height,
67        }
68    }
69}
70
71impl DelegatableNode for TextModifierNode {
72    fn node_state(&self) -> &NodeState {
73        &self.state
74    }
75}
76
77impl ModifierNode for TextModifierNode {
78    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
79        // Invalidate layout and draw when text node is attached
80        context.invalidate(InvalidationKind::Layout);
81        context.invalidate(InvalidationKind::Draw);
82        context.invalidate(InvalidationKind::Semantics);
83    }
84
85    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
86        Some(self)
87    }
88
89    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
90        Some(self)
91    }
92
93    fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
94        Some(self)
95    }
96
97    fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
98        Some(self)
99    }
100
101    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
102        Some(self)
103    }
104
105    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
106        Some(self)
107    }
108}
109
110impl LayoutModifierNode for TextModifierNode {
111    fn measure(
112        &self,
113        _context: &mut dyn ModifierNodeContext,
114        _measurable: &dyn Measurable,
115        constraints: Constraints,
116    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
117        // Measure the text content
118        let text_size = self.measure_text_content();
119
120        // Constrain text size to the provided constraints
121        let width = text_size
122            .width
123            .clamp(constraints.min_width, constraints.max_width);
124        let height = text_size
125            .height
126            .clamp(constraints.min_height, constraints.max_height);
127
128        // Text is a leaf node - return the text size directly with no offset
129        // We don't call measurable.measure() because there's no wrapped content
130        // (Text uses EmptyMeasurePolicy which has no children)
131        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
132    }
133
134    fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
135        self.measure_text_content().width
136    }
137
138    fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
139        self.measure_text_content().width
140    }
141
142    fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
143        self.measure_text_content().height
144    }
145
146    fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
147        self.measure_text_content().height
148    }
149
150    fn create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
151        Some(Box::new(TextMeasurementProxy {
152            text: self.text.clone(),
153        }))
154    }
155}
156
157/// Measurement proxy for TextModifierNode that snapshots live state.
158///
159/// Phase 2: Instead of reconstructing nodes via `TextModifierNode::new()`, this proxy
160/// directly implements measurement logic using the snapshotted text content.
161struct TextMeasurementProxy {
162    text: Rc<str>,
163}
164
165impl TextMeasurementProxy {
166    /// Measure the text content dimensions.
167    /// Matches TextModifierNode::measure_text_content() logic.
168    fn measure_text_content(&self) -> Size {
169        let metrics = crate::text::measure_text(&self.text);
170        Size {
171            width: metrics.width,
172            height: metrics.height,
173        }
174    }
175}
176
177impl MeasurementProxy for TextMeasurementProxy {
178    fn measure_proxy(
179        &self,
180        _context: &mut dyn ModifierNodeContext,
181        _measurable: &dyn Measurable,
182        constraints: Constraints,
183    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
184        // Directly implement text measurement logic (no node reconstruction)
185        let text_size = self.measure_text_content();
186
187        // Constrain text size to the provided constraints
188        let width = text_size
189            .width
190            .clamp(constraints.min_width, constraints.max_width);
191        let height = text_size
192            .height
193            .clamp(constraints.min_height, constraints.max_height);
194
195        // Text is a leaf node - return the text size directly with no offset
196        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
197    }
198
199    fn min_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
200        self.measure_text_content().width
201    }
202
203    fn max_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
204        self.measure_text_content().width
205    }
206
207    fn min_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
208        self.measure_text_content().height
209    }
210
211    fn max_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
212        self.measure_text_content().height
213    }
214}
215
216impl DrawModifierNode for TextModifierNode {
217    fn draw(&self, _draw_scope: &mut dyn DrawScope) {
218        // In a full implementation, this would:
219        // 1. Get the text paragraph layout cache
220        // 2. Paint the text using draw_scope canvas
221        //
222        // For now, this is a placeholder. The actual rendering will be handled
223        // by the renderer which can read text from the modifier chain.
224        //
225        // Future: Implement actual text drawing here using DrawScope
226    }
227}
228
229impl SemanticsNode for TextModifierNode {
230    fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
231        // Provide text content for accessibility
232        config.content_description = Some(self.text.to_string());
233    }
234}
235
236/// Element that creates and updates TextModifierNode instances.
237///
238/// This follows the modifier element pattern where the element is responsible for:
239/// - Creating new nodes (via `create`)
240/// - Updating existing nodes when properties change (via `update`)
241/// - Declaring capabilities (LAYOUT | DRAW | SEMANTICS)
242///
243/// Matches Jetpack Compose: `TextStringSimpleElement` in BasicText.kt
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct TextModifierElement {
246    text: Rc<str>,
247}
248
249impl TextModifierElement {
250    pub fn new(text: Rc<str>) -> Self {
251        Self { text }
252    }
253}
254
255impl Hash for TextModifierElement {
256    fn hash<H: Hasher>(&self, state: &mut H) {
257        self.text.hash(state);
258    }
259}
260
261impl ModifierNodeElement for TextModifierElement {
262    type Node = TextModifierNode;
263
264    fn create(&self) -> Self::Node {
265        TextModifierNode::new(self.text.clone())
266    }
267
268    fn update(&self, node: &mut Self::Node) {
269        if node.text != self.text {
270            node.text = self.text.clone();
271            // Text changed - need to invalidate layout, draw, and semantics
272            // Note: In the full implementation, we would call context.invalidate here
273            // but update() doesn't currently have access to context.
274            // The invalidation will happen on the next recomposition when the node
275            // is reconciled.
276        }
277    }
278
279    fn capabilities(&self) -> NodeCapabilities {
280        // Text nodes participate in layout, drawing, and semantics
281        NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
282    }
283}