1use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
8use jag_surface::Canvas;
9
10use super::types::WidgetColors;
11
12const CARET_BLINK_RATE: f32 = 1.0;
13
14#[derive(Debug, Clone)]
17pub struct TextInput {
18 text: String,
19 caret_timer: f32,
20 placeholder: String,
21 text_size: f32,
22}
23
24impl TextInput {
25 pub fn new(placeholder: impl Into<String>, text_size: f32) -> Self {
26 Self {
27 text: String::new(),
28 caret_timer: 0.0,
29 placeholder: placeholder.into(),
30 text_size,
31 }
32 }
33
34 pub fn push_str(&mut self, s: &str) {
37 self.text.push_str(s);
38 self.reset_caret();
39 }
40
41 pub fn push_char(&mut self, c: char) {
42 self.text.push(c);
43 self.reset_caret();
44 }
45
46 pub fn pop_char(&mut self) -> Option<char> {
47 let c = self.text.pop();
48 self.reset_caret();
49 c
50 }
51
52 pub fn clear(&mut self) {
53 self.text.clear();
54 self.reset_caret();
55 }
56
57 pub fn set_text(&mut self, s: String) {
58 self.text = s;
59 self.reset_caret();
60 }
61
62 pub fn text(&self) -> &str {
65 &self.text
66 }
67
68 pub fn is_empty(&self) -> bool {
69 self.text.is_empty()
70 }
71
72 pub fn update_blink(&mut self, dt: f32) {
75 self.caret_timer = (self.caret_timer + dt) % CARET_BLINK_RATE;
76 }
77
78 pub fn reset_caret(&mut self) {
79 self.caret_timer = 0.0;
80 }
81
82 fn caret_visible(&self) -> bool {
83 self.caret_timer < CARET_BLINK_RATE * 0.5
84 }
85
86 #[allow(clippy::too_many_arguments)]
95 pub fn render(
96 &self,
97 canvas: &mut Canvas,
98 x: f32,
99 y: f32,
100 w: f32,
101 h: f32,
102 bg: ColorLinPremul,
103 border: Option<ColorLinPremul>,
104 focused: bool,
105 corner_radius: f32,
106 display_prefix: Option<&str>,
107 colors: &WidgetColors,
108 z: i32,
109 ) {
110 let pad = if corner_radius > 0.0 { 6.0 } else { 4.0 };
111
112 if corner_radius > 0.0 {
114 let rrect = RoundedRect {
115 rect: Rect { x, y, w, h },
116 radii: RoundedRadii {
117 tl: corner_radius,
118 tr: corner_radius,
119 br: corner_radius,
120 bl: corner_radius,
121 },
122 };
123 canvas.rounded_rect(rrect, Brush::Solid(bg), z);
124 } else {
125 canvas.fill_rect(x, y, w, h, Brush::Solid(bg), z);
126 }
127
128 if let Some(bc) = border {
130 if corner_radius > 0.0 {
131 let rrect = RoundedRect {
132 rect: Rect { x, y, w, h },
133 radii: RoundedRadii {
134 tl: corner_radius,
135 tr: corner_radius,
136 br: corner_radius,
137 bl: corner_radius,
138 },
139 };
140 canvas.stroke_rounded_rect(rrect, 1.0, Brush::Solid(bc), z + 1);
141 } else {
142 let b = 1.0;
143 canvas.fill_rect(x, y, w, b, Brush::Solid(bc), z + 1);
144 canvas.fill_rect(x, y + h - b, w, b, Brush::Solid(bc), z + 1);
145 canvas.fill_rect(x, y, b, h, Brush::Solid(bc), z + 1);
146 canvas.fill_rect(x + w - b, y, b, h, Brush::Solid(bc), z + 1);
147 }
148 }
149
150 let text_y = y + h / 2.0 + self.text_size * 0.35;
152 let text_x = x + pad;
153
154 if self.text.is_empty() && display_prefix.is_none() {
155 canvas.draw_text_run(
156 [text_x, text_y],
157 self.placeholder.clone(),
158 self.text_size,
159 colors.text_muted,
160 z + 2,
161 );
162 } else {
163 let display = match display_prefix {
164 Some(pre) => format!("{pre}{}", self.text),
165 None => self.text.clone(),
166 };
167 let color = if self.text.is_empty() {
168 colors.text_muted
169 } else {
170 colors.text
171 };
172 canvas.draw_text_run([text_x, text_y], display, self.text_size, color, z + 2);
173 }
174
175 if focused && self.caret_visible() {
177 let display_text = match display_prefix {
178 Some(pre) if !self.text.is_empty() => format!("{pre}{}", self.text),
179 _ => self.text.clone(),
180 };
181 let text_w = if display_text.is_empty() {
182 0.0
183 } else {
184 canvas.measure_text_width(&display_text, self.text_size)
185 };
186 let caret_x = text_x + text_w;
187 let caret_y = y + 3.0;
188 let caret_h = h - 6.0;
189 canvas.fill_rect(
190 caret_x,
191 caret_y,
192 1.5,
193 caret_h,
194 Brush::Solid(colors.text),
195 z + 3,
196 );
197 }
198 }
199}
200
201#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn new_input_is_empty() {
211 let input = TextInput::new("Search...", 14.0);
212 assert!(input.is_empty());
213 assert_eq!(input.text(), "");
214 }
215
216 #[test]
217 fn push_and_pop() {
218 let mut input = TextInput::new("", 14.0);
219 input.push_char('a');
220 input.push_char('b');
221 assert_eq!(input.text(), "ab");
222 assert_eq!(input.pop_char(), Some('b'));
223 assert_eq!(input.text(), "a");
224 }
225
226 #[test]
227 fn push_str_appends() {
228 let mut input = TextInput::new("", 14.0);
229 input.push_str("hello");
230 assert_eq!(input.text(), "hello");
231 }
232
233 #[test]
234 fn clear_empties() {
235 let mut input = TextInput::new("", 14.0);
236 input.push_str("data");
237 input.clear();
238 assert!(input.is_empty());
239 }
240
241 #[test]
242 fn set_text_replaces() {
243 let mut input = TextInput::new("", 14.0);
244 input.push_str("old");
245 input.set_text("new".to_string());
246 assert_eq!(input.text(), "new");
247 }
248
249 #[test]
250 fn caret_blink_cycle() {
251 let mut input = TextInput::new("", 14.0);
252 assert!(input.caret_visible());
254 input.update_blink(CARET_BLINK_RATE * 0.5);
256 assert!(!input.caret_visible());
257 input.update_blink(CARET_BLINK_RATE * 0.5);
259 assert!(input.caret_visible());
260 }
261
262 #[test]
263 fn reset_caret_makes_visible() {
264 let mut input = TextInput::new("", 14.0);
265 input.update_blink(CARET_BLINK_RATE * 0.75);
266 assert!(!input.caret_visible());
267 input.reset_caret();
268 assert!(input.caret_visible());
269 }
270}