Skip to main content

cranpose_ui/widgets/
basic_text_field.rs

1//! BasicTextField widget for editable text input.
2//!
3//! This module provides the `BasicTextField` composable following Jetpack Compose's
4//! `BasicTextField` pattern from `compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt`.
5
6#![allow(non_snake_case)]
7
8use crate::composable;
9use crate::layout::policies::EmptyMeasurePolicy;
10use crate::modifier::Modifier;
11use crate::text::TextStyle; // Add import
12use crate::text_field_modifier_node::TextFieldElement;
13use crate::widgets::Layout;
14use cranpose_core::NodeId;
15use cranpose_foundation::modifier_element;
16use cranpose_foundation::text::{TextFieldLineLimits, TextFieldState};
17use cranpose_ui_graphics::Color;
18///
19/// # When to use
20/// Use this when you need an editable text input but want full control over the
21/// styling (no built-in borders or labels).
22///
23/// # Arguments
24///
25/// * `state` - The observable text field state that holds text content and cursor position.
26/// * `modifier` - Modifiers for styling and layout.
27/// * `style` - Text styling (color, font size).
28///
29/// # Example
30///
31/// ```rust,ignore
32/// let text = remember_text_field_state("Initial text");
33/// BasicTextField(text, Modifier::padding(8.0), TextStyle::default());
34/// ```
35#[composable]
36pub fn BasicTextField(state: TextFieldState, modifier: Modifier, style: TextStyle) -> NodeId {
37    BasicTextFieldWithOptions(
38        state,
39        modifier,
40        BasicTextFieldOptions {
41            text_style: style,
42            ..BasicTextFieldOptions::default()
43        },
44    )
45}
46
47/// Options for customizing BasicTextField appearance and behavior.
48#[derive(Debug, Clone, PartialEq)]
49pub struct BasicTextFieldOptions {
50    /// Text style
51    pub text_style: TextStyle,
52    /// Cursor color
53    pub cursor_color: Color,
54    /// Line limits: SingleLine or MultiLine with optional min/max
55    pub line_limits: TextFieldLineLimits,
56}
57
58impl Default for BasicTextFieldOptions {
59    fn default() -> Self {
60        Self {
61            text_style: TextStyle::default(),
62            cursor_color: Color(0.0, 0.0, 0.0, 1.0), // Black
63            line_limits: TextFieldLineLimits::default(),
64        }
65    }
66}
67
68/// Creates an editable text field with custom options.
69///
70/// This is the full version of `BasicTextField` with all configuration options.
71#[composable]
72pub fn BasicTextFieldWithOptions(
73    state: TextFieldState,
74    modifier: Modifier,
75    options: BasicTextFieldOptions,
76) -> NodeId {
77    // Read text to create composition dependency.
78    // TextFieldState now uses mutableStateOf internally, so this read
79    // automatically creates composition dependency via the snapshot system.
80    let _text = state.text();
81
82    // Build the text field element with line limits
83    let text_field_element = TextFieldElement::new(state, options.text_style)
84        .with_cursor_color(options.cursor_color)
85        .with_line_limits(options.line_limits);
86
87    // Wrap it in a modifier
88    let text_field_modifier = modifier_element(text_field_element);
89    let final_modifier = Modifier::from_parts(vec![text_field_modifier]);
90    let combined_modifier = modifier.then(final_modifier);
91
92    // Use EmptyMeasurePolicy - TextFieldModifierNode handles all measurement
93    // This matches Jetpack Compose's BasicTextField architecture
94    Layout(
95        combined_modifier,
96        EmptyMeasurePolicy,
97        || {}, // No children
98    )
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use cranpose_core::{location_key, Composition, DefaultScheduler, MemoryApplier, Runtime};
105    use std::sync::Arc;
106
107    /// Sets up a test runtime and keeps it alive for the duration of the test.
108    fn with_test_runtime<T>(f: impl FnOnce() -> T) -> T {
109        let _runtime = Runtime::new(Arc::new(DefaultScheduler));
110        f()
111    }
112
113    #[test]
114    fn basic_text_field_creates_node() {
115        let mut composition = Composition::new(MemoryApplier::new());
116        let state = TextFieldState::new("Test content");
117
118        let result = composition.render(location_key(file!(), line!(), column!()), {
119            let state = state.clone();
120            move || {
121                BasicTextField(state.clone(), Modifier::empty(), TextStyle::default());
122            }
123        });
124
125        assert!(result.is_ok());
126        assert!(composition.root().is_some());
127    }
128
129    #[test]
130    fn basic_text_field_state_updates() {
131        with_test_runtime(|| {
132            let state = TextFieldState::new("Hello");
133            assert_eq!(state.text(), "Hello");
134
135            state.edit(|buffer| {
136                buffer.place_cursor_at_end();
137                buffer.insert("!");
138            });
139
140            assert_eq!(state.text(), "Hello!");
141        });
142    }
143}