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