1use std::cell::Cell;
2use std::time::Instant;
3
4use arboard::Clipboard;
5use glyphon::Metrics;
6use glyphon::cosmic_text::Align;
7use taffy::prelude::*;
8use winit::event::{ElementState, MouseButton, WindowEvent, KeyEvent};
9use winit::keyboard::{Key, ModifiersState, NamedKey};
10
11use crate::framework::{DrawContext, EventContext, Widget};
12use crate::signal::SetSignal;
13
14pub struct TextInput {
15 text: String,
16 cursor_pos: usize,
17 selection: Option<(usize, usize)>,
18 on_change: Option<SetSignal<String>>,
19 metrics: Metrics,
20 text_color: [u8; 3],
21 placeholder_color: [u8; 3],
22 placeholder: String,
23 background: [f32; 4],
24 focus_border_color: [f32; 4],
25 border_radius: f32,
26 padding: f32,
27 focused: bool,
28 last_input_time: Instant,
29 pub(crate) cursor_pixel_x: f32,
31 selection_lo_px: f32,
33 selection_hi_px: f32,
34 char_edges: Vec<f32>,
36 mouse_dragging: bool,
38 last_mouse_x: f32,
40 text_cmd_index: Cell<Option<usize>>,
42}
43
44impl TextInput {
45 pub fn new(on_change: SetSignal<String>) -> Self {
46 Self {
47 text: String::new(),
48 cursor_pos: 0,
49 selection: None,
50 on_change: Some(on_change),
51 metrics: Metrics::new(16.0, 22.0),
52 text_color: [230, 230, 230],
53 placeholder_color: [120, 120, 140],
54 placeholder: String::new(),
55 background: [0.12, 0.16, 0.22, 1.0],
56 focus_border_color: [0.3, 0.6, 0.9, 1.0],
57 border_radius: 6.0,
58 padding: 10.0,
59 focused: false,
60 last_input_time: Instant::now(),
61 cursor_pixel_x: 0.0,
62 selection_lo_px: 0.0,
63 selection_hi_px: 0.0,
64 char_edges: Vec::new(),
65 mouse_dragging: false,
66 last_mouse_x: 0.0,
67 text_cmd_index: Cell::new(None),
68 }
69 }
70
71 pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
72 self.placeholder = text.into();
73 self
74 }
75
76 pub fn with_metrics(mut self, metrics: Metrics) -> Self {
77 self.metrics = metrics;
78 self
79 }
80
81 pub fn with_text_color(mut self, color: [u8; 3]) -> Self {
82 self.text_color = color;
83 self
84 }
85
86 pub fn with_background(mut self, color: [f32; 4]) -> Self {
87 self.background = color;
88 self
89 }
90
91 pub fn with_border_radius(mut self, radius: f32) -> Self {
92 self.border_radius = radius;
93 self
94 }
95
96 pub fn with_padding(mut self, padding: f32) -> Self {
97 self.padding = padding;
98 self
99 }
100
101 pub fn with_initial_value(mut self, text: impl Into<String>) -> Self {
102 self.text = text.into();
103 self.cursor_pos = self.text.len();
104 self
105 }
106
107 fn notify_change(&self) {
108 if let Some(ref sig) = self.on_change {
109 sig.set(self.text.clone());
110 }
111 }
112
113 fn insert_text(&mut self, s: &str) {
114 self.delete_selection();
115 let byte_pos = self.cursor_byte_pos();
116 self.text.insert_str(byte_pos, s);
117 self.cursor_pos += s.chars().count();
118 self.last_input_time = Instant::now();
119 self.notify_change();
120 }
121
122 fn delete_back(&mut self) {
123 if self.delete_selection() {
124 return;
125 }
126 if self.cursor_pos > 0 {
127 self.cursor_pos -= 1;
128 let byte_pos = self.cursor_byte_pos();
129 self.text.remove(byte_pos);
130 self.last_input_time = Instant::now();
131 self.notify_change();
132 }
133 }
134
135 fn delete_forward(&mut self) {
136 if self.delete_selection() {
137 return;
138 }
139 let char_count = self.text.chars().count();
140 if self.cursor_pos < char_count {
141 let byte_pos = self.cursor_byte_pos();
142 self.text.remove(byte_pos);
143 self.last_input_time = Instant::now();
144 self.notify_change();
145 }
146 }
147
148 fn delete_selection(&mut self) -> bool {
149 if let Some((start, end)) = self.selection.take() {
150 let (lo, hi) = if start < end { (start, end) } else { (end, start) };
151 let lo_byte = self.char_to_byte(lo);
152 let hi_byte = self.char_to_byte(hi);
153 self.text.replace_range(lo_byte..hi_byte, "");
154 self.cursor_pos = lo;
155 self.last_input_time = Instant::now();
156 self.notify_change();
157 true
158 } else {
159 false
160 }
161 }
162
163 fn move_cursor(&mut self, delta: i32, shift: bool) {
164 let char_count = self.text.chars().count();
165 let old_pos = self.cursor_pos;
166
167 if delta < 0 {
168 self.cursor_pos = self.cursor_pos.saturating_sub((-delta) as usize);
169 } else {
170 self.cursor_pos = (self.cursor_pos + delta as usize).min(char_count);
171 }
172
173 if shift {
174 match self.selection {
175 None => self.selection = Some((old_pos, self.cursor_pos)),
176 Some((anchor, _)) => self.selection = Some((anchor, self.cursor_pos)),
177 }
178 } else {
179 self.selection = None;
180 }
181
182 self.last_input_time = Instant::now();
183 }
184
185 fn select_all(&mut self) {
186 let char_count = self.text.chars().count();
187 self.selection = Some((0, char_count));
188 self.cursor_pos = char_count;
189 }
190
191 fn selected_text(&self) -> Option<String> {
192 let (start, end) = self.selection?;
193 let (lo, hi) = if start < end { (start, end) } else { (end, start) };
194 let lo_byte = self.char_to_byte(lo);
195 let hi_byte = self.char_to_byte(hi);
196 Some(self.text[lo_byte..hi_byte].to_string())
197 }
198
199 fn copy_selection(&self) {
200 if let Some(text) = self.selected_text() {
201 if let Ok(mut cb) = Clipboard::new() {
202 let _ = cb.set_text(text);
203 }
204 }
205 }
206
207 fn cut_selection(&mut self) {
208 self.copy_selection();
209 self.delete_selection();
210 }
211
212 fn paste(&mut self) {
213 if let Ok(mut cb) = Clipboard::new() {
214 if let Ok(text) = cb.get_text() {
215 self.insert_text(&text);
216 }
217 }
218 }
219
220 fn cursor_byte_pos(&self) -> usize {
221 self.char_to_byte(self.cursor_pos)
222 }
223
224 fn char_to_byte(&self, char_pos: usize) -> usize {
225 self.text
226 .char_indices()
227 .nth(char_pos)
228 .map(|(i, _)| i)
229 .unwrap_or(self.text.len())
230 }
231
232 fn cursor_visible(&self) -> bool {
233 let elapsed = self.last_input_time.elapsed().as_millis();
234 (elapsed % 1060) < 530
235 }
236
237 pub fn text_before_cursor(&self) -> &str {
239 let byte_pos = self.cursor_byte_pos();
240 &self.text[..byte_pos]
241 }
242
243 pub fn text(&self) -> &str {
245 &self.text
246 }
247
248 pub fn cursor(&self) -> usize {
250 self.cursor_pos
251 }
252
253 pub fn is_focused(&self) -> bool {
255 self.focused
256 }
257
258 fn char_pos_from_x(&self, layout: &Layout, x: f32) -> usize {
260 let text_x = layout.location.x + self.padding;
261 let rel_x = x - text_x;
262 if self.char_edges.is_empty() {
263 return 0;
264 }
265 let mut best = 0;
267 let mut best_dist = f32::MAX;
268 for (i, &edge) in self.char_edges.iter().enumerate() {
269 let dist = (edge - rel_x).abs();
270 if dist < best_dist {
271 best_dist = dist;
272 best = i;
273 }
274 }
275 best
276 }
277
278 fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
279 x >= layout.location.x
280 && x <= layout.location.x + layout.size.width
281 && y >= layout.location.y
282 && y <= layout.location.y + layout.size.height
283 }
284}
285
286impl Widget for TextInput {
287 fn style(&self) -> Style {
288 let height = self.metrics.line_height + self.padding * 2.0;
289 Style {
290 size: Size {
291 width: Dimension::Percent(1.0),
292 height: Dimension::Length(height),
293 },
294 flex_shrink: 0.0,
295 ..Default::default()
296 }
297 }
298
299 fn draw(&self, ctx: &mut DrawContext) {
300 let layout = ctx.layout;
301 let x = layout.location.x;
302 let y = layout.location.y;
303 let w = layout.size.width;
304 let h = layout.size.height;
305
306 let border_w = if self.focused { 1.5 } else { 0.0 };
308 let border_c = if self.focused {
309 self.focus_border_color
310 } else {
311 [0.0; 4]
312 };
313 ctx.renderer.fill_rect_styled(
314 (x, y, w, h),
315 self.background,
316 self.border_radius,
317 border_w,
318 border_c,
319 );
320
321 let text_x = x + self.padding;
322 let text_y = y + self.padding;
323 let text_w = (w - self.padding * 2.0).max(0.0);
324 let text_h = (h - self.padding * 2.0).max(0.0);
325
326 if let Some((start, end)) = self.selection {
328 let (lo, hi) = if start < end { (start, end) } else { (end, start) };
329 if lo != hi {
330 let sel_x0 = text_x + self.selection_lo_px;
331 let sel_x1 = text_x + self.selection_hi_px;
332 let sel_w = (sel_x1 - sel_x0).max(0.0);
333 ctx.renderer.fill_rect_rounded(
334 (sel_x0, text_y, sel_w, text_h),
335 [0.2, 0.4, 0.7, 0.5],
336 2.0,
337 );
338 }
339 }
340
341 if self.text.is_empty() && !self.placeholder.is_empty() {
343 self.text_cmd_index.set(None);
344 ctx.renderer.draw_text(
345 &self.placeholder,
346 (text_x, text_y),
347 self.placeholder_color,
348 (text_w, text_h),
349 self.metrics,
350 Align::Left,
351 );
352 } else {
353 let char_count = self.text.chars().count();
355 let measure: Vec<usize> = (0..=char_count).collect();
356 let idx = ctx.renderer.draw_text_measured(
357 &self.text,
358 (text_x, text_y),
359 self.text_color,
360 (text_w, text_h),
361 self.metrics,
362 Align::Left,
363 measure,
364 );
365 self.text_cmd_index.set(Some(idx));
366 }
367
368 if self.focused && self.cursor_visible() {
370 let cursor_x = text_x + self.cursor_pixel_x;
371 let cursor_h = self.metrics.font_size;
372 let cursor_y = text_y + (text_h - cursor_h) * 0.5;
373 ctx.renderer.fill_rect_rounded(
374 (cursor_x, cursor_y, 1.5, cursor_h),
375 [0.4, 0.7, 1.0, 1.0],
376 0.0,
377 );
378 }
379 }
380
381 fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
382 let layout = ctx.layout;
383 match ctx.event {
384 WindowEvent::MouseInput {
385 state: ElementState::Pressed,
386 button: MouseButton::Left,
387 ..
388 } => {
389 if self.hit_test(layout, self.last_mouse_x, layout.location.y + 1.0) {
390 let pos = self.char_pos_from_x(layout, self.last_mouse_x);
391 self.cursor_pos = pos;
392 self.selection = None;
393 self.mouse_dragging = true;
394 self.last_input_time = Instant::now();
395 true
396 } else {
397 false
398 }
399 }
400 WindowEvent::MouseInput {
401 state: ElementState::Released,
402 button: MouseButton::Left,
403 ..
404 } => {
405 self.mouse_dragging = false;
406 false
407 }
408 WindowEvent::CursorMoved { position, .. } => {
409 self.last_mouse_x = position.x as f32;
410 if self.mouse_dragging && self.focused {
411 let pos = self.char_pos_from_x(layout, position.x as f32);
412 if pos != self.cursor_pos {
413 let anchor = match self.selection {
414 Some((anchor, _)) => anchor,
415 None => self.cursor_pos,
416 };
417 self.cursor_pos = pos;
418 if anchor != pos {
419 self.selection = Some((anchor, pos));
420 } else {
421 self.selection = None;
422 }
423 self.last_input_time = Instant::now();
424 }
425 true
426 } else {
427 false
428 }
429 }
430 _ => false,
431 }
432 }
433
434 fn handle_key_event(&mut self, event: &KeyEvent, modifiers: ModifiersState) -> bool {
435 let ctrl = modifiers.control_key();
436 let shift = modifiers.shift_key();
437
438 if ctrl {
439 match &event.logical_key {
440 Key::Character(c) if c.as_str() == "a" => {
441 self.select_all();
442 return true;
443 }
444 Key::Character(c) if c.as_str() == "c" => {
445 self.copy_selection();
446 return true;
447 }
448 Key::Character(c) if c.as_str() == "v" => {
449 self.paste();
450 return true;
451 }
452 Key::Character(c) if c.as_str() == "x" => {
453 self.cut_selection();
454 return true;
455 }
456 _ => {}
457 }
458 }
459
460 match &event.logical_key {
461 Key::Character(c) => {
462 if !ctrl {
463 self.insert_text(c.as_str());
464 return true;
465 }
466 false
467 }
468 Key::Named(NamedKey::Backspace) => {
469 self.delete_back();
470 true
471 }
472 Key::Named(NamedKey::Delete) => {
473 self.delete_forward();
474 true
475 }
476 Key::Named(NamedKey::ArrowLeft) => {
477 self.move_cursor(-1, shift);
478 true
479 }
480 Key::Named(NamedKey::ArrowRight) => {
481 self.move_cursor(1, shift);
482 true
483 }
484 Key::Named(NamedKey::Home) => {
485 let old = self.cursor_pos;
486 self.cursor_pos = 0;
487 if shift {
488 match self.selection {
489 None => self.selection = Some((old, 0)),
490 Some((anchor, _)) => self.selection = Some((anchor, 0)),
491 }
492 } else {
493 self.selection = None;
494 }
495 self.last_input_time = Instant::now();
496 true
497 }
498 Key::Named(NamedKey::End) => {
499 let old = self.cursor_pos;
500 let end = self.text.chars().count();
501 self.cursor_pos = end;
502 if shift {
503 match self.selection {
504 None => self.selection = Some((old, end)),
505 Some((anchor, _)) => self.selection = Some((anchor, end)),
506 }
507 } else {
508 self.selection = None;
509 }
510 self.last_input_time = Instant::now();
511 true
512 }
513 Key::Named(NamedKey::Tab) => false,
514 Key::Named(NamedKey::Enter) => false,
515 _ => false,
516 }
517 }
518
519 fn update_measures(&mut self, measures: &[Vec<f32>]) {
520 if let Some(idx) = self.text_cmd_index.get() {
521 if let Some(edges) = measures.get(idx) {
522 self.char_edges = edges.clone();
523 if let Some(&w) = edges.get(self.cursor_pos) {
525 self.cursor_pixel_x = w;
526 }
527 if let Some((start, end)) = self.selection {
529 let (lo, hi) = if start < end { (start, end) } else { (end, start) };
530 self.selection_lo_px = edges.get(lo).copied().unwrap_or(0.0);
531 self.selection_hi_px = edges.get(hi).copied().unwrap_or(0.0);
532 }
533 }
534 }
535 }
536
537 fn is_focusable(&self) -> bool {
538 true
539 }
540
541 fn set_focus(&mut self, focused: bool) {
542 self.focused = focused;
543 if focused {
544 self.last_input_time = Instant::now();
545 }
546 }
547}