cranpose_foundation/text/
line_limits.rs

1//! Line limit configuration for text fields.
2//!
3//! This module provides `TextFieldLineLimits` which controls whether a text field
4//! is single-line (horizontal scroll, no newlines) or multi-line with optional
5//! min/max line constraints.
6//!
7//! Matches Jetpack Compose's `TextFieldLineLimits` from
8//! `compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldLineLimits.kt`.
9
10/// Line limit configuration for text fields.
11///
12/// Controls whether a text field allows multiple lines of input and how many
13/// lines are visible at minimum and maximum.
14///
15/// # SingleLine
16///
17/// When `SingleLine` is used:
18/// - Newline characters (`\n`) are blocked from input
19/// - Pasted text has newlines replaced with spaces
20/// - The text field scrolls horizontally if content exceeds width
21/// - The Enter key does NOT insert a newline (may trigger submit action)
22///
23/// # MultiLine
24///
25/// When `MultiLine` is used:
26/// - Newline characters are allowed
27/// - The text field scrolls vertically if content exceeds visible lines
28/// - `min_lines` controls minimum visible height (default: 1)
29/// - `max_lines` controls maximum visible height before scrolling (default: unlimited)
30///
31/// # Example
32///
33/// ```
34/// use cranpose_foundation::text::TextFieldLineLimits;
35///
36/// // Single-line text field (like a search box)
37/// let single = TextFieldLineLimits::SingleLine;
38///
39/// // Multi-line with default settings
40/// let multi = TextFieldLineLimits::default();
41///
42/// // Multi-line with 3-5 visible lines
43/// let constrained = TextFieldLineLimits::MultiLine { min_lines: 3, max_lines: 5 };
44/// ```
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum TextFieldLineLimits {
47    /// Single line input - no newlines allowed, horizontal scrolling.
48    SingleLine,
49    /// Multi-line input with optional line constraints.
50    ///
51    /// - `min_lines`: Minimum number of visible lines (affects minimum height)
52    /// - `max_lines`: Maximum number of visible lines before scrolling
53    MultiLine {
54        /// Minimum visible lines (default: 1)
55        min_lines: usize,
56        /// Maximum visible lines before scrolling (default: unlimited)
57        max_lines: usize,
58    },
59}
60
61impl TextFieldLineLimits {
62    /// Default multi-line with no constraints (1 line minimum, unlimited maximum).
63    pub const DEFAULT: Self = Self::MultiLine {
64        min_lines: 1,
65        max_lines: usize::MAX,
66    };
67
68    /// Returns true if this is single-line mode.
69    #[inline]
70    pub fn is_single_line(&self) -> bool {
71        matches!(self, Self::SingleLine)
72    }
73
74    /// Returns true if this is multi-line mode.
75    #[inline]
76    pub fn is_multi_line(&self) -> bool {
77        matches!(self, Self::MultiLine { .. })
78    }
79
80    /// Returns the minimum number of lines (1 for SingleLine).
81    pub fn min_lines(&self) -> usize {
82        match self {
83            Self::SingleLine => 1,
84            Self::MultiLine { min_lines, .. } => *min_lines,
85        }
86    }
87
88    /// Returns the maximum number of lines (1 for SingleLine, configured value for MultiLine).
89    pub fn max_lines(&self) -> usize {
90        match self {
91            Self::SingleLine => 1,
92            Self::MultiLine { max_lines, .. } => *max_lines,
93        }
94    }
95}
96
97impl Default for TextFieldLineLimits {
98    fn default() -> Self {
99        Self::DEFAULT
100    }
101}
102
103/// Filters text for single-line mode by replacing newlines with spaces.
104///
105/// This is used when:
106/// - Pasting text into a SingleLine text field
107/// - Programmatically setting text on a SingleLine field
108///
109/// # Example
110///
111/// ```
112/// use cranpose_foundation::text::filter_for_single_line;
113///
114/// assert_eq!(filter_for_single_line("hello\nworld"), "hello world");
115/// assert_eq!(filter_for_single_line("a\n\nb"), "a  b");
116/// ```
117pub fn filter_for_single_line(text: &str) -> String {
118    text.replace('\n', " ")
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn single_line_properties() {
127        let limits = TextFieldLineLimits::SingleLine;
128        assert!(limits.is_single_line());
129        assert!(!limits.is_multi_line());
130        assert_eq!(limits.min_lines(), 1);
131        assert_eq!(limits.max_lines(), 1);
132    }
133
134    #[test]
135    fn multi_line_default_properties() {
136        let limits = TextFieldLineLimits::default();
137        assert!(!limits.is_single_line());
138        assert!(limits.is_multi_line());
139        assert_eq!(limits.min_lines(), 1);
140        assert_eq!(limits.max_lines(), usize::MAX);
141    }
142
143    #[test]
144    fn multi_line_constrained_properties() {
145        let limits = TextFieldLineLimits::MultiLine {
146            min_lines: 3,
147            max_lines: 10,
148        };
149        assert!(!limits.is_single_line());
150        assert!(limits.is_multi_line());
151        assert_eq!(limits.min_lines(), 3);
152        assert_eq!(limits.max_lines(), 10);
153    }
154
155    #[test]
156    fn filter_replaces_newlines() {
157        assert_eq!(filter_for_single_line("hello\nworld"), "hello world");
158        assert_eq!(filter_for_single_line("a\n\nb"), "a  b");
159        assert_eq!(filter_for_single_line("no newlines"), "no newlines");
160        assert_eq!(filter_for_single_line("\n\n\n"), "   ");
161    }
162}