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