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}