1use std::cell::RefCell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length, Rect, Size};
5use sequoia_input::{
6 CursorIcon, Event, EventPhase, ImeEvent, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
7};
8use sequoia_layout::{LayoutCtx, LayoutStyle};
9use sequoia_reactive::Signal;
10use sequoia_style::{TextFieldStyle, Theme, VisualState};
11use sequoia_view::{AnyView, EventCtx, FontWeight, PaintCtx, Unit, View};
12
13use crate::buffer::BufferEngine;
14use crate::clipboard::SharedClipboard;
15use crate::underline;
16
17const HEIGHT: f32 = 26.0;
18
19const CARET_W: f32 = 1.5;
20
21#[derive(Clone, Default)]
22struct FieldState {
23 caret: usize,
24
25 anchor: Option<usize>,
26
27 bounds: Vec<f32>,
28
29 preedit: String,
30
31 preedit_cursor: Option<(usize, usize)>,
32
33 caret_rect: Option<Rect>,
34
35 was_focused: bool,
36
37 scroll: f32,
38
39 session_text: Option<String>,
40
41 engine: BufferEngine,
42}
43
44pub struct TextField {
45 value: Signal<String>,
46 placeholder: String,
47 max_length: Option<usize>,
48 font_size: Option<f32>,
49 weight: Option<FontWeight>,
50 text_color: Option<Color>,
51 line_color: Option<Color>,
52 line_width: Option<f32>,
53 appearance: Option<TextFieldStyle>,
54 placeholder_color: Option<Color>,
55 background: Option<Color>,
56 border_color: Option<Color>,
57 border_width: Option<f32>,
58 radius: Option<f32>,
59 padding: Option<(f32, f32)>,
60 style: LayoutStyle,
61 margin: Edges,
62
63 secure: bool,
64
65 readonly: bool,
66
67 disabled: bool,
68
69 error: bool,
70
71 revision: Signal<u32>,
72
73 state: Rc<RefCell<FieldState>>,
74
75 on_edit_start: Option<Rc<dyn Fn()>>,
76
77 on_edit_end: Option<Rc<dyn Fn(String)>>,
78
79 on_submit: Option<Rc<dyn Fn(String)>>,
80}
81
82impl TextField {
83 pub fn new(value: Signal<String>) -> Self {
84 Self {
85 value,
86 placeholder: String::new(),
87 max_length: None,
88 font_size: None,
89 weight: None,
90 text_color: None,
91 line_color: None,
92 line_width: None,
93 appearance: None,
94 placeholder_color: None,
95 background: None,
96 border_color: None,
97 border_width: None,
98 radius: None,
99 padding: None,
100 style: LayoutStyle {
101 width: Length::Fill(1.0),
102 height: Length::Px(HEIGHT),
103 ..LayoutStyle::default()
104 },
105 margin: Edges::ZERO,
106 secure: false,
107 readonly: false,
108 disabled: false,
109 error: false,
110 revision: Signal::new(0),
111 state: Rc::new(RefCell::new(FieldState::default())),
112 on_edit_start: None,
113 on_edit_end: None,
114 on_submit: None,
115 }
116 }
117
118 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
119 self.placeholder = placeholder.into();
120 self
121 }
122
123 pub fn max_length(mut self, max: usize) -> Self {
124 self.max_length = Some(max);
125 self
126 }
127
128 pub fn secure(mut self, secure: bool) -> Self {
132 self.secure = secure;
133 self
134 }
135
136 pub fn password(self, password: bool) -> Self {
137 self.secure(password)
138 }
139
140 pub fn readonly(mut self, readonly: bool) -> Self {
142 self.readonly = readonly;
143 self
144 }
145
146 pub fn disabled(mut self, disabled: bool) -> Self {
147 self.disabled = disabled;
148 self
149 }
150
151 pub fn error(mut self, error: bool) -> Self {
153 self.error = error;
154 self
155 }
156
157 pub fn auto_size(mut self, auto: bool) -> Self {
159 self.style.height = if auto {
160 Length::Fit
161 } else {
162 Length::Px(HEIGHT)
163 };
164 self
165 }
166
167 pub fn on_edit_start(mut self, f: impl Fn() + 'static) -> Self {
168 self.on_edit_start = Some(Rc::new(f));
169 self
170 }
171
172 pub fn on_edit_end(mut self, f: impl Fn(String) + 'static) -> Self {
173 self.on_edit_end = Some(Rc::new(f));
174 self
175 }
176
177 pub fn on_submit(mut self, f: impl Fn(String) + 'static) -> Self {
178 self.on_submit = Some(Rc::new(f));
179 self
180 }
181
182 pub fn font_size(mut self, size: f32) -> Self {
183 self.font_size = Some(size);
184 self
185 }
186
187 pub fn weight(mut self, weight: FontWeight) -> Self {
188 self.weight = Some(weight);
189 self
190 }
191
192 pub fn bold(mut self, bold: bool) -> Self {
193 self.weight = Some(if bold {
194 FontWeight::BOLD
195 } else {
196 FontWeight::NORMAL
197 });
198 self
199 }
200
201 pub fn text_color(mut self, color: Color) -> Self {
202 self.text_color = Some(color);
203 self
204 }
205
206 pub fn line_color(mut self, color: Color) -> Self {
207 self.line_color = Some(color);
208 self
209 }
210
211 pub fn appearance(mut self, style: TextFieldStyle) -> Self {
212 self.appearance = Some(style);
213 self
214 }
215
216 pub fn placeholder_color(mut self, color: Color) -> Self {
217 self.placeholder_color = Some(color);
218 self
219 }
220
221 pub fn line_width(mut self, width: f32) -> Self {
222 self.line_width = Some(width.max(0.0));
223 self
224 }
225
226 pub fn background(mut self, color: Color) -> Self {
227 self.background = Some(color);
228 self
229 }
230
231 pub fn border_color(mut self, color: Color) -> Self {
232 self.border_color = Some(color);
233 self
234 }
235
236 pub fn border_width(mut self, width: f32) -> Self {
237 self.border_width = Some(width.max(0.0));
238 self
239 }
240
241 pub fn border(mut self, width: f32, color: Color) -> Self {
242 self.border_width = Some(width.max(0.0));
243 self.border_color = Some(color);
244 self
245 }
246
247 pub fn radius(mut self, radius: f32) -> Self {
248 self.radius = Some(radius.max(0.0));
249 self
250 }
251
252 pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
253 let p = padding.into();
254 self.padding = Some(((p.left + p.right) * 0.5, (p.top + p.bottom) * 0.5));
255 self
256 }
257
258 pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
259 self.padding = Some((x.max(0.0), y.max(0.0)));
260 self
261 }
262
263 pub fn width(mut self, width: impl Into<Length>) -> Self {
264 self.style = self.style.width(width.into());
265 self
266 }
267
268 pub fn height(mut self, height: impl Into<Length>) -> Self {
269 self.style = self.style.height(height.into());
270 self
271 }
272
273 pub fn min_width(mut self, v: f32) -> Self {
274 self.style = self.style.min_width(v);
275 self
276 }
277
278 pub fn max_width(mut self, v: f32) -> Self {
279 self.style = self.style.max_width(v);
280 self
281 }
282
283 pub fn min_height(mut self, v: f32) -> Self {
284 self.style = self.style.min_height(v);
285 self
286 }
287
288 pub fn max_height(mut self, v: f32) -> Self {
289 self.style = self.style.max_height(v);
290 self
291 }
292
293 pub fn flex(mut self, weight: f32) -> Self {
294 self.style = self.style.flex(weight);
295 self
296 }
297
298 pub fn expand(self) -> Self {
299 self.flex(1.0)
300 }
301
302 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
303 let m = margin.into();
304 self.margin = Edges::only(
305 sanitize_margin(m.left),
306 sanitize_margin(m.top),
307 sanitize_margin(m.right),
308 sanitize_margin(m.bottom),
309 );
310 self
311 }
312
313 pub fn margin_xy(self, x: f32, y: f32) -> Self {
314 self.margin(Edges::xy(x, y))
315 }
316
317 pub fn margin_x(mut self, x: f32) -> Self {
318 let x = sanitize_margin(x);
319 self.margin.left = x;
320 self.margin.right = x;
321 self
322 }
323
324 pub fn margin_y(mut self, y: f32) -> Self {
325 let y = sanitize_margin(y);
326 self.margin.top = y;
327 self.margin.bottom = y;
328 self
329 }
330
331 pub fn margin_left(mut self, v: f32) -> Self {
332 self.margin.left = sanitize_margin(v);
333 self
334 }
335
336 pub fn margin_right(mut self, v: f32) -> Self {
337 self.margin.right = sanitize_margin(v);
338 self
339 }
340
341 pub fn margin_top(mut self, v: f32) -> Self {
342 self.margin.top = sanitize_margin(v);
343 self
344 }
345
346 pub fn margin_bottom(mut self, v: f32) -> Self {
347 self.margin.bottom = sanitize_margin(v);
348 self
349 }
350
351 fn with_state<R>(&self, f: impl FnOnce(&mut FieldState) -> R) -> R {
352 f(&mut self.state.borrow_mut())
353 }
354
355 fn chars(&self) -> Vec<char> {
356 self.value.get_untracked().chars().collect()
357 }
358
359 pub fn caret(&self) -> usize {
360 self.with_state(|s| s.caret)
361 }
362
363 pub fn selection(&self) -> Option<(usize, usize)> {
364 self.with_state(|s| {
365 s.anchor.map(|a| {
366 let c = s.caret;
367 (a.min(c), a.max(c))
368 })
369 })
370 }
371
372 fn begin_session(&self) {
373 let cur = self.value.get_untracked();
374 self.with_state(|s| {
375 s.session_text = Some(cur);
376 s.was_focused = true;
377 });
378 if let Some(f) = &self.on_edit_start {
379 f();
380 }
381 }
382
383 fn note_focus(&self, focused: bool) {
384 enum Trans {
385 Start,
386 End(String),
387 None,
388 }
389 let cur = self.value.get_untracked();
390 let trans = self.with_state(|s| {
391 let was = s.was_focused;
392 s.was_focused = focused;
393 if !was && focused {
394 s.session_text = Some(cur.clone());
395 Trans::Start
396 } else if was && !focused {
397 match s.session_text.take() {
398 Some(before) if before != cur => Trans::End(cur.clone()),
399 _ => Trans::None,
400 }
401 } else {
402 Trans::None
403 }
404 });
405 match trans {
406 Trans::Start => {
407 if let Some(f) = &self.on_edit_start {
408 f();
409 }
410 }
411 Trans::End(text) => {
412 if let Some(f) = &self.on_edit_end {
413 f(text);
414 }
415 }
416 Trans::None => {}
417 }
418 }
419
420 fn stored_bounds(&self) -> Vec<f32> {
421 self.with_state(|s| s.bounds.clone())
422 }
423
424 fn store_bounds(&self, bounds: Vec<f32>) {
425 self.with_state(|s| s.bounds = bounds);
426 }
427
428 fn set_caret(&self, caret: usize) {
429 let value = self.value.clone();
430 self.with_state(|s| {
431 s.engine.ensure(&value);
432 s.caret = caret.min(s.engine.len_chars());
433 s.anchor = None;
434 });
435 }
436
437 fn set_caret_extend(&self, caret: usize, extend: bool) {
438 let value = self.value.clone();
439 self.with_state(|s| {
440 s.engine.ensure(&value);
441 let caret = caret.min(s.engine.len_chars());
442 if extend {
443 let anchor = s.anchor.unwrap_or(s.caret);
444 s.anchor = Some(anchor);
445 s.caret = caret;
446 if s.anchor == Some(s.caret) {
447 s.anchor = None;
448 }
449 } else {
450 s.caret = caret;
451 s.anchor = None;
452 }
453 });
454 }
455
456 fn bump(&self) {
457 self.revision
458 .set(self.revision.get_untracked().wrapping_add(1));
459 }
460
461 fn font_weight(&self) -> FontWeight {
462 self.weight
463 .or_else(|| {
464 self.appearance
465 .and_then(|s| s.font_weight)
466 .map(FontWeight::new)
467 })
468 .unwrap_or_default()
469 }
470
471 fn resolved_font_size(&self, theme: &Theme) -> f32 {
472 self.font_size
473 .or_else(|| self.appearance.and_then(|s| s.font_size))
474 .unwrap_or(theme.font_size)
475 }
476
477 fn resolved_line_width(&self) -> f32 {
478 if let Some(w) = self
479 .line_width
480 .or_else(|| self.appearance.and_then(|s| s.line_width))
481 {
482 return w;
483 }
484 let has_line_color =
485 self.line_color.is_some() || self.appearance.and_then(|s| s.line).is_some();
486 if has_line_color {
487 return underline::LINE_W;
488 }
489 0.0
490 }
491
492 fn resolved_text_color(&self) -> Option<Color> {
493 self.text_color
494 .or_else(|| self.appearance.and_then(|s| s.text))
495 }
496
497 fn resolved_line_color(&self) -> Option<Color> {
498 self.line_color
499 .or_else(|| self.appearance.and_then(|s| s.line))
500 }
501
502 fn resolved_placeholder_color(&self, theme: &Theme) -> Color {
503 self.placeholder_color
504 .or_else(|| self.appearance.and_then(|s| s.placeholder))
505 .unwrap_or(theme.text_secondary)
506 }
507
508 fn resolved_pad(&self) -> (f32, f32) {
509 self.padding
510 .or_else(|| self.appearance.and_then(|s| s.padding))
511 .unwrap_or((underline::PAD_X, 0.0))
512 }
513
514 fn resolved_background(&self) -> Color {
515 self.background
516 .or_else(|| self.appearance.and_then(|s| s.background))
517 .unwrap_or(Color::TRANSPARENT)
518 }
519
520 fn resolved_border(&self, theme: &Theme) -> Option<(f32, Color)> {
521 let width_opt = self
522 .border_width
523 .or_else(|| self.appearance.and_then(|s| s.border_width));
524 let color_opt = self
525 .border_color
526 .or_else(|| self.appearance.and_then(|s| s.border));
527 match (width_opt, color_opt) {
528 (None, None) => None,
529 (Some(w), Some(c)) => {
530 let w = w.max(0.0);
531 if w <= 0.0 || c.a <= 0.0 {
532 None
533 } else {
534 Some((w, c))
535 }
536 }
537 (Some(w), None) => {
538 let w = w.max(0.0);
539 if w <= 0.0 || theme.border.a <= 0.0 {
540 None
541 } else {
542 Some((w, theme.border))
543 }
544 }
545 (None, Some(c)) => {
546 if c.a <= 0.0 {
547 None
548 } else {
549 Some((1.0, c))
550 }
551 }
552 }
553 }
554
555 fn resolved_radius(&self, theme: &Theme) -> f32 {
556 self.radius
557 .or_else(|| self.appearance.and_then(|s| s.radius))
558 .unwrap_or(theme.radius)
559 .max(0.0)
560 }
561
562 fn approx_border_w(&self) -> f32 {
563 if let Some(w) = self
564 .border_width
565 .or_else(|| self.appearance.and_then(|s| s.border_width))
566 {
567 return w.max(0.0);
568 }
569 let has_color =
570 self.border_color.is_some() || self.appearance.and_then(|s| s.border).is_some();
571 if has_color { 1.0 } else { 0.0 }
572 }
573
574 fn caret_at(&self, x: f32, len: usize) -> usize {
575 let bounds = self.stored_bounds();
576 let Some(last) = bounds.last() else {
577 return len;
578 };
579 if x >= *last {
580 return len;
581 }
582 if x <= 0.0 {
583 return 0;
584 }
585 let mut best = 0;
586 let mut best_distance = f32::MAX;
587 for (i, offset) in bounds.iter().enumerate() {
588 let distance = (offset - x).abs();
589 if distance < best_distance {
590 best_distance = distance;
591 best = i;
592 }
593 }
594 best.min(len)
595 }
596
597 fn delete_selection(&self) -> bool {
598 let sel = self.selection();
599 let Some((a, b)) = sel else { return false };
600 if a == b {
601 self.with_state(|s| s.anchor = None);
602 return false;
603 }
604 let value = self.value.clone();
605 self.with_state(|s| {
606 s.engine.ensure(&value);
607 let len = s.engine.len_chars();
608 let (a, b) = (a.min(len), b.min(len));
609 let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
610 s.engine.delete(ab..bb);
611 s.engine.commit(&value);
612 s.caret = s.engine.to_chars(ab);
613 s.anchor = None;
614 });
615 self.bump();
616 true
617 }
618
619 pub fn selected_text(&self) -> Option<String> {
620 let (a, b) = self.selection()?;
621 if a == b {
622 return None;
623 }
624 let value = self.value.clone();
625 self.with_state(|s| {
626 s.engine.ensure(&value);
627 let len = s.engine.len_chars();
628 let (a, b) = (a.min(len), b.min(len));
629 let text = s
630 .engine
631 .buffer()
632 .substring(s.engine.to_bytes(a)..s.engine.to_bytes(b));
633 if text.is_empty() { None } else { Some(text) }
634 })
635 }
636
637 pub fn select_range(&self, a: usize, b: usize) {
638 let value = self.value.clone();
639 self.with_state(|s| {
640 s.engine.ensure(&value);
641 let len = s.engine.len_chars();
642 let (a, b) = (a.min(len), b.min(len));
643 s.caret = b;
644 s.anchor = if a == b { None } else { Some(a) };
645 });
646 self.bump();
647 }
648
649 pub fn clear_selection(&self) -> bool {
650 let had = self.with_state(|s| s.anchor.take().is_some());
651 if had {
652 self.bump();
653 }
654 had
655 }
656
657 fn insert(&self, c: char) {
658 self.insert_str(&c.to_string());
659 }
660
661 fn insert_str(&self, text: &str) {
662 if text.is_empty() || self.readonly || self.disabled {
663 return;
664 }
665
666 let value = self.value.clone();
667 self.with_state(|st| {
668 st.engine.ensure(&value);
669 let total = st.engine.len_chars();
670
671 let sel_bytes = match st.anchor.map(|a| (a.min(st.caret), a.max(st.caret))) {
672 None => None,
673 Some((a, b)) if a == b => {
674 st.anchor = None;
675 None
676 }
677 Some((a, b)) => {
678 let (a, b) = (a.min(total), b.min(total));
679 Some((st.engine.to_bytes(a), st.engine.to_bytes(b)))
680 }
681 };
682 let allowed = match self.max_length {
683 Some(max) => max.saturating_sub(total),
684 None => usize::MAX,
685 };
686 let ins = BufferEngine::fit_prefix(text, allowed);
687 let caret_b = match sel_bytes {
688 Some((ab, bb)) => {
689 st.engine.replace(ab..bb, ins);
690 ab + ins.len()
691 }
692 None => {
693 let caret_b = st.engine.to_bytes(st.caret.min(total));
694 st.engine.insert(caret_b, ins);
695 caret_b + ins.len()
696 }
697 };
698 st.engine.commit(&value);
699 st.caret = st.engine.to_chars(caret_b).min(st.engine.len_chars());
700 st.anchor = None;
701 });
702 self.bump();
703 }
704
705 fn backspace(&self) {
706 if self.readonly || self.disabled {
707 return;
708 }
709 if self.delete_selection() {
710 return;
711 }
712 let value = self.value.clone();
713 self.with_state(|s| {
714 s.engine.ensure(&value);
715 let caret = s.caret.min(s.engine.len_chars());
716 if caret == 0 {
717 return;
718 }
719 let caret_b = s.engine.to_bytes(caret);
720 let from_b = sequoia_buffer::prev_grapheme(s.engine.buffer(), caret_b);
721 s.engine.delete(from_b..caret_b);
722 s.engine.commit(&value);
723 s.caret = s.engine.to_chars(from_b);
724 s.anchor = None;
725 });
726 self.bump();
727 }
728
729 fn delete(&self) {
730 if self.readonly || self.disabled {
731 return;
732 }
733 if self.delete_selection() {
734 return;
735 }
736 let value = self.value.clone();
737 self.with_state(|s| {
738 s.engine.ensure(&value);
739 let len = s.engine.len_chars();
740 let caret = s.caret.min(len);
741 if caret >= len {
742 return;
743 }
744 let caret_b = s.engine.to_bytes(caret);
745 let to_b = sequoia_buffer::next_grapheme(s.engine.buffer(), caret_b);
746 s.engine.delete(caret_b..to_b);
747 s.engine.commit(&value);
748 s.caret = s.engine.to_chars(caret_b);
749 s.anchor = None;
750 });
751 self.bump();
752 }
753
754 fn move_caret(&self, delta: isize, extend: bool) {
755 let value = self.value.clone();
756 let next = self.with_state(|s| {
757 s.engine.ensure(&value);
758 let mut caret_b = s.engine.to_bytes(s.caret.min(s.engine.len_chars()));
759 let steps = delta.unsigned_abs().max(1);
760 for _ in 0..steps {
761 let b = if delta < 0 {
762 sequoia_buffer::prev_grapheme(s.engine.buffer(), caret_b)
763 } else {
764 sequoia_buffer::next_grapheme(s.engine.buffer(), caret_b)
765 };
766 if b == caret_b {
767 break;
768 }
769 caret_b = b;
770 }
771 s.engine.to_chars(caret_b)
772 });
773 self.set_caret_extend(next, extend);
774 self.bump();
775 }
776
777 fn move_home(&self, extend: bool) {
778 self.set_caret_extend(0, extend);
779 self.bump();
780 }
781
782 fn move_end(&self, extend: bool) {
783 let value = self.value.clone();
784 let end = self.with_state(|s| {
785 s.engine.ensure(&value);
786 s.engine.len_chars()
787 });
788 self.set_caret_extend(end, extend);
789 self.bump();
790 }
791
792 pub fn select_all(&self) {
793 let value = self.value.clone();
794 self.with_state(|s| {
795 s.engine.ensure(&value);
796 let len = s.engine.len_chars();
797 s.anchor = Some(0);
798 s.caret = len;
799 });
800 self.bump();
801 }
802
803 pub fn submit(&self) {
804 let Some(cb) = self.on_submit.clone() else {
805 return;
806 };
807 let has_preedit = self.with_state(|s| !s.preedit.is_empty());
808 if has_preedit {
809 return;
810 }
811 let cur = self.value.get_untracked();
812 self.with_state(|s| {
813 s.session_text = Some(cur.clone());
814 });
815 cb(cur);
816 }
817
818 fn undo(&self) {
819 let value = self.value.clone();
820 let caret = self.with_state(|s| {
821 s.engine.ensure(&value);
822 match s.engine.undo() {
823 Some((a, _)) => {
824 s.engine.commit(&value);
825 Some(s.engine.to_chars(a))
826 }
827 None => None,
828 }
829 });
830 if let Some(caret) = caret {
831 self.with_state(|s| {
832 s.caret = caret;
833 s.anchor = None;
834 });
835 self.bump();
836 }
837 }
838
839 fn redo(&self) {
840 let value = self.value.clone();
841 let caret = self.with_state(|s| {
842 s.engine.ensure(&value);
843 match s.engine.redo() {
844 Some((_, b)) => {
845 s.engine.commit(&value);
846 Some(s.engine.to_chars(b))
847 }
848 None => None,
849 }
850 });
851 if let Some(caret) = caret {
852 self.with_state(|s| {
853 s.caret = caret;
854 s.anchor = None;
855 });
856 self.bump();
857 }
858 }
859
860 fn displayed(&self, text: &str) -> String {
861 if self.secure {
862 "\u{2022}".repeat(text.chars().count())
863 } else {
864 text.to_string()
865 }
866 }
867
868 fn move_word(&self, dir: isize, extend: bool) {
869 let value = self.value.clone();
870 let next = self.with_state(|s| {
871 s.engine.ensure(&value);
872 let len = s.engine.len_chars();
873 let caret_b = s.engine.to_bytes(s.caret.min(len));
874 let next_b = if dir < 0 {
875 sequoia_buffer::word_start_before(s.engine.buffer(), caret_b)
876 } else {
877 sequoia_buffer::word_end_after(s.engine.buffer(), caret_b)
878 };
879 s.engine.to_chars(next_b)
880 });
881 self.set_caret_extend(next, extend);
882 self.bump();
883 }
884
885 fn delete_word(&self, back: bool) {
886 if self.readonly || self.disabled {
887 return;
888 }
889 if self.delete_selection() {
890 return;
891 }
892 let value = self.value.clone();
893 self.with_state(|s| {
894 s.engine.ensure(&value);
895 let len = s.engine.len_chars();
896 let caret_b = s.engine.to_bytes(s.caret.min(len));
897 let (from_b, to_b) = if back {
898 (
899 sequoia_buffer::word_start_before(s.engine.buffer(), caret_b),
900 caret_b,
901 )
902 } else {
903 (
904 caret_b,
905 sequoia_buffer::word_end_after(s.engine.buffer(), caret_b),
906 )
907 };
908 if from_b < to_b {
909 s.engine.delete(from_b..to_b);
910 s.engine.commit(&value);
911 s.caret = s.engine.to_chars(from_b).min(s.engine.len_chars());
912 s.anchor = None;
913 }
914 });
915 self.bump();
916 }
917
918 fn on_key(&self, key: &KeyEvent) {
919 let extend = key.modifiers.contains(Modifiers::SHIFT);
920 let composing = self.with_state(|s| !s.preedit.is_empty());
921 let len = self.with_state(|s| {
922 s.engine.ensure(&self.value);
923 s.engine.len_chars()
924 });
925 match key.key {
926 Key::ENTER => self.submit(),
927 Key::BACKSPACE => {
928 if key.modifiers.contains(Modifiers::CONTROL) {
929 self.delete_word(true);
930 } else {
931 self.backspace();
932 }
933 }
934 Key::DELETE => {
935 if key.modifiers.contains(Modifiers::CONTROL) {
936 self.delete_word(false);
937 } else {
938 self.delete();
939 }
940 }
941 Key::ARROW_LEFT => {
942 if key.modifiers.contains(Modifiers::CONTROL)
943 || key.modifiers.contains(Modifiers::ALT)
944 {
945 self.move_word(-1, extend);
946 } else {
947 self.move_caret(-1, extend);
948 }
949 }
950 Key::ARROW_RIGHT => {
951 if key.modifiers.contains(Modifiers::CONTROL)
952 || key.modifiers.contains(Modifiers::ALT)
953 {
954 self.move_word(1, extend);
955 } else {
956 self.move_caret(1, extend);
957 }
958 }
959 Key::HOME | Key::PAGE_UP => {
960 if extend {
961 self.move_home(true);
962 } else {
963 self.set_caret(0);
964 self.bump();
965 }
966 }
967 Key::END | Key::PAGE_DOWN => {
968 if extend {
969 self.move_end(true);
970 } else {
971 self.set_caret(len);
972 self.bump();
973 }
974 }
975 Key::SPACE => {
976 if composing {
977 return;
978 }
979 if extend {
980 self.move_caret(1, true);
981 } else {
982 self.insert(' ');
983 }
984 }
985
986 Key::ESCAPE | Key::TAB => {
987 let had = self.with_state(|s| {
988 let had = s.anchor.is_some() || !s.preedit.is_empty();
989 s.anchor = None;
990 s.preedit.clear();
991 had
992 });
993 if had {
994 self.bump();
995 }
996 }
997 other => {
998 if let Some(c) = other.char()
999 && !c.is_control()
1000 {
1001 let cmd = key.modifiers.contains(Modifiers::CONTROL)
1002 || key.modifiers.contains(Modifiers::SUPER);
1003 if cmd || composing {
1004 return;
1005 }
1006 self.insert(c);
1007 }
1008 }
1009 }
1010 }
1011
1012 fn on_ime(&self, ime: &ImeEvent) {
1013 if self.disabled {
1014 return;
1015 }
1016 match ime {
1017 ImeEvent::Enabled => {}
1018 ImeEvent::Preedit { value, cursor } => {
1019 if self.readonly || self.secure {
1020 return;
1021 }
1022 self.with_state(|s| {
1023 s.preedit = value.clone();
1024 s.preedit_cursor = *cursor;
1025 });
1026 self.bump();
1027 }
1028 ImeEvent::Commit(value) => {
1029 self.with_state(|s| {
1030 s.preedit.clear();
1031 s.preedit_cursor = None;
1032 });
1033 if !self.readonly {
1034 self.insert_str(value);
1035 }
1036 }
1037 ImeEvent::Disabled => {
1038 let had = self.with_state(|s| {
1039 let had = !s.preedit.is_empty();
1040 s.preedit.clear();
1041 s.preedit_cursor = None;
1042 had
1043 });
1044 if had {
1045 self.bump();
1046 }
1047 }
1048
1049 _ => {}
1050 }
1051 }
1052
1053 fn on_clipboard_key(&self, key: &KeyEvent, ctx: &EventCtx) -> bool {
1054 let cmd =
1055 key.modifiers.contains(Modifiers::CONTROL) || key.modifiers.contains(Modifiers::SUPER);
1056 if !cmd {
1057 return false;
1058 }
1059 let Some(c) = key.key.char().map(|c| c.to_ascii_lowercase()) else {
1060 return false;
1061 };
1062
1063 if c == 'z' && !key.modifiers.contains(Modifiers::SHIFT) {
1064 self.undo();
1065 return true;
1066 }
1067 if c == 'y' || (c == 'z' && key.modifiers.contains(Modifiers::SHIFT)) {
1068 self.redo();
1069 return true;
1070 }
1071 if !matches!(c, 'c' | 'x' | 'v' | 'a') {
1072 return false;
1073 }
1074 if c == 'a' {
1075 self.select_all();
1076 return true;
1077 }
1078 if self.secure && matches!(c, 'c' | 'x') {
1079 return true;
1080 }
1081 if self.readonly && matches!(c, 'x' | 'v') {
1082 return true;
1083 }
1084 let Some(clip) = ctx.environment.get::<SharedClipboard>() else {
1085 return true;
1086 };
1087 match c {
1088 'c' => {
1089 if let Some(sel) = self.selected_text() {
1090 clip.0.set_text(&sel);
1091 } else {
1092 clip.0.set_text(&self.value.get_untracked());
1093 }
1094 }
1095 'x' => {
1096 if let Some(sel) = self.selected_text() {
1097 clip.0.set_text(&sel);
1098 self.delete_selection();
1099 } else {
1100 clip.0.set_text(&self.value.get_untracked());
1101 self.value.set(String::new());
1102 self.set_caret(0);
1103 self.bump();
1104 }
1105 }
1106 _ => {
1107 if let Some(paste) = clip.0.get_text() {
1108 let clean: String = paste
1109 .chars()
1110 .filter_map(|ch| {
1111 if ch == '\n' || ch == '\r' {
1112 Some(' ')
1113 } else if ch.is_control() {
1114 None
1115 } else {
1116 Some(ch)
1117 }
1118 })
1119 .collect();
1120 self.insert_str(&clean);
1121 }
1122 }
1123 }
1124 true
1125 }
1126}
1127
1128impl View for TextField {
1129 fn body(&self) -> impl View + 'static {
1130 Unit
1131 }
1132
1133 fn is_leaf(&self) -> bool {
1134 true
1135 }
1136
1137 fn accessibility(&self) -> Option<sequoia_view::Accessible> {
1138 let text = self.value.get_untracked();
1139 Some(
1140 sequoia_view::Accessible::new(
1141 sequoia_view::AccessibleRole::TextField,
1142 if self.placeholder.is_empty() {
1143 "pole tekstowe".to_string()
1144 } else {
1145 self.placeholder.clone()
1146 },
1147 )
1148 .value(self.displayed(&text))
1149 .caret(Some(self.caret()))
1150 .selection(self.selection())
1151 .disabled(self.disabled),
1152 )
1153 }
1154
1155 fn focusable(&self) -> bool {
1156 !self.disabled
1157 }
1158
1159 fn accepts_text_input(&self) -> bool {
1160 !self.disabled
1161 }
1162
1163 fn ime_caret_rect(&self) -> Option<Rect> {
1164 self.with_state(|s| s.caret_rect)
1165 }
1166
1167 fn has_visual_state(&self) -> bool {
1168 true
1169 }
1170
1171 fn reuse_state(&self, old: &Self) {
1172 if !Rc::ptr_eq(&self.state, &old.state) {
1173 *self.state.borrow_mut() = old.state.borrow().clone();
1174 }
1175 self.revision.set(old.revision.get_untracked());
1176 }
1177
1178 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
1179 let _ = self.value.get();
1180 let _ = self.revision.get();
1181
1182 Some(Vec::new())
1183 }
1184
1185 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
1186 if self.disabled {
1187 CursorIcon::Default
1188 } else {
1189 CursorIcon::Text
1190 }
1191 }
1192
1193 fn layout_style(&self) -> LayoutStyle {
1194 let mut style = self.style;
1195 style.margin = self.margin;
1196 style
1197 }
1198
1199 fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
1200 let appearance = self.appearance.unwrap_or_default();
1201 let size = self.font_size.or(appearance.font_size).unwrap_or(14.0);
1202 let weight = self.font_weight();
1203 let line_width = self.resolved_line_width();
1204 let (pad_x, pad_y) = self.resolved_pad();
1205 let bw = self.approx_border_w();
1206 let w = ctx
1207 .text_width_weighted(&self.value.get_untracked(), size, weight)
1208 .max(ctx.text_width_weighted(&self.placeholder, size, weight));
1209 let h = if line_width > 0.0 {
1210 underline::content_height(size, line_width)
1211 } else {
1212 LayoutCtx::text_line_height(size)
1213 };
1214 Some(Size::new(
1215 w + pad_x * 2.0 + bw * 2.0,
1216 h + pad_y * 2.0 + bw * 2.0,
1217 ))
1218 }
1219
1220 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
1221 if self.disabled || ctx.phase != EventPhase::Target {
1222 return;
1223 }
1224 match event {
1225 Event::Pointer(PointerEvent::Down {
1226 button: PointerButton::Left,
1227 pos,
1228 modifiers,
1229 ..
1230 }) => {
1231 self.begin_session();
1232 let len = self.chars().len();
1233 let (pad_x, _) = self.resolved_pad();
1234 let ix = pad_x + self.approx_border_w();
1235 let scroll = self.with_state(|s| s.scroll);
1236 let x = pos.x - (ctx.rect.left() + ix) + scroll;
1237 let caret = self.caret_at(x, len);
1238 let extend = modifiers.contains(Modifiers::SHIFT);
1239 self.set_caret_extend(caret, extend);
1240 self.bump();
1241 ctx.request_focus();
1242
1243 ctx.capture_pointer();
1244 ctx.handled();
1245 }
1246 Event::Pointer(PointerEvent::Move { pos, .. }) => {
1247 if ctx.state.pressed {
1248 let len = self.chars().len();
1249 let (pad_x, _) = self.resolved_pad();
1250 let ix = pad_x + self.approx_border_w();
1251 let scroll = self.with_state(|s| s.scroll);
1252 let x = pos.x - (ctx.rect.left() + ix) + scroll;
1253 let caret = self.caret_at(x, len);
1254 self.set_caret_extend(caret, true);
1255 self.bump();
1256 ctx.handled();
1257 }
1258 }
1259 Event::Key(key) => {
1260 if !self.on_clipboard_key(key, ctx) {
1261 self.on_key(key);
1262 }
1263 ctx.handled();
1264 }
1265 Event::Ime(ime) => {
1266 self.on_ime(ime);
1267 ctx.handled();
1268 }
1269 _ => {}
1270 }
1271 }
1272
1273 fn paint_self(&self, ctx: &mut PaintCtx) {
1274 self.note_focus(ctx.state.focused);
1275 let rect = ctx.rect;
1276 if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
1277 return;
1278 }
1279 let theme = ctx.theme();
1280 let size = self.resolved_font_size(&theme);
1281 let weight = self.font_weight();
1282 let line_width = self.resolved_line_width();
1283 let placeholder_color = self.resolved_placeholder_color(&theme);
1284 let (pad_x, pad_y) = self.resolved_pad();
1285 let background = self.resolved_background();
1286 let radius = self.resolved_radius(&theme);
1287 let border = if self.error {
1288 Some((1.5, sequoia_style::FORM_ERROR))
1289 } else {
1290 self.resolved_border(&theme)
1291 };
1292 let colors = underline::field_colors(
1293 &theme,
1294 ctx.state.hovered || ctx.state.focused,
1295 ctx.state.focused,
1296 self.resolved_text_color(),
1297 self.resolved_line_color(),
1298 );
1299 let text_color = if self.disabled {
1300 theme.disabled
1301 } else {
1302 colors.text
1303 };
1304 let placeholder_color = if self.disabled {
1305 theme.disabled
1306 } else {
1307 placeholder_color
1308 };
1309 let line_color = if self.disabled {
1310 theme.disabled
1311 } else {
1312 colors.line
1313 };
1314
1315 if background.a > 0.0 || border.is_some() {
1316 match border {
1317 Some((w, color)) => {
1318 ctx.bordered_quad(rect, background, radius, w, color);
1319 }
1320 None => {
1321 ctx.quad(rect, background, radius);
1322 }
1323 }
1324 }
1325
1326 let bw = border.map(|(w, _)| w).unwrap_or(0.0);
1327 let content_h = if line_width > 0.0 {
1328 underline::content_height(size, line_width)
1329 } else {
1330 LayoutCtx::text_line_height(size)
1331 };
1332 let inner_h = (rect.size.height - bw * 2.0 - pad_y * 2.0).max(0.0);
1333 let yoff = if inner_h > content_h {
1334 (inner_h - content_h) * 0.5
1335 } else {
1336 0.0
1337 };
1338 let (ipx, ipy) = (pad_x + bw, pad_y + bw + yoff);
1339
1340 let line_w = (rect.size.width - ipx * 2.0).max(0.0);
1341 if line_width > 0.0 && line_color.a > 0.0 {
1342 let line = underline::line_rect_padded(rect, size, line_w, line_width, ipx, ipy);
1343 ctx.quad(line, line_color, line_width * 0.5);
1344 }
1345
1346 let text = self.value.get_untracked();
1347 let shown = self.displayed(&text);
1348 let chars: Vec<char> = shown.chars().collect();
1349 let (caret, anchor, preedit, preedit_cursor) = self.with_state(|s| {
1350 (s.caret, s.anchor, s.preedit.clone(), s.preedit_cursor)
1351 });
1352 let caret = caret.min(chars.len());
1353 let selection = anchor.map(|a| (a.min(caret), a.max(caret)));
1354 let composing = !preedit.is_empty();
1355 let comp_start = selection.map(|(a, _)| a).unwrap_or(caret);
1356 let comp_end = selection.map(|(_, b)| b).unwrap_or(caret);
1357 let vis_w = (rect.size.width - ipx * 2.0).max(0.0);
1358
1359 let mut caret_x = 0.0;
1360 let mut bounds: Vec<f32>;
1361 if ctx.state.hovered || ctx.state.focused || anchor.is_some() {
1362 bounds = ctx.caret_positions(&shown, size, weight);
1363
1364 if composing {
1365 let pw = ctx.text_width_weighted(&preedit, size, weight);
1366 for b in bounds.iter_mut().skip(comp_end) {
1367 *b += pw;
1368 }
1369 let base = bounds.get(comp_start).copied().unwrap_or(0.0);
1370 let inside = match preedit_cursor {
1371 Some((cur, _)) => {
1372 let mut i = cur.min(preedit.len());
1373 while i > 0 && !preedit.is_char_boundary(i) {
1374 i -= 1;
1375 }
1376 ctx.text_width_weighted(&preedit[..i], size, weight)
1377 }
1378 None => ctx.text_width_weighted(&preedit, size, weight),
1379 };
1380 caret_x = base + inside;
1381 } else {
1382 caret_x = bounds.get(caret).copied().unwrap_or(0.0);
1383 }
1384
1385 let text_w = bounds.last().copied().unwrap_or(0.0);
1386 self.with_state(|s| {
1387 if caret_x + CARET_W - s.scroll > vis_w {
1388 s.scroll = caret_x + CARET_W - vis_w;
1389 }
1390 if caret_x - s.scroll < 0.0 {
1391 s.scroll = caret_x;
1392 }
1393 s.scroll = s.scroll.clamp(0.0, (text_w + CARET_W - vis_w).max(0.0));
1394 });
1395 self.store_bounds(bounds);
1396 }
1397 let scroll = self.with_state(|s| s.scroll);
1398 let x = rect.left() + ipx - scroll;
1399 let y = underline::baseline_padded(rect, size, ipy);
1400
1401 let clip_box = Rect::from_xywh(
1402 rect.left() + ipx,
1403 rect.top() + bw,
1404 vis_w,
1405 (rect.size.height - bw * 2.0).max(0.0),
1406 );
1407 ctx.push_clip(clip_box);
1408
1409 if let Some(a) = anchor.filter(|_| !composing) {
1410 let (s0, s1) = (a.min(caret).min(chars.len()), a.max(caret).min(chars.len()));
1411 if s1 > s0 {
1412 let stored = self.stored_bounds();
1413
1414 let x0 = stored.get(s0).copied().unwrap_or(0.0);
1415 let x1 = stored.get(s1).copied().unwrap_or(x0);
1416 if x1 > x0 {
1417 let top = y - size * 0.8;
1418 let sel = Rect::from_xywh(x + x0, top, x1 - x0, size * 1.05);
1419 ctx.quad(sel, theme.accent_pressed, 0.0);
1420 }
1421 }
1422 }
1423
1424 if chars.is_empty() && preedit.is_empty() && !self.placeholder.is_empty() {
1425 ctx.draw_text_weighted(&self.placeholder, (x, y), size, weight, placeholder_color);
1426 } else if preedit.is_empty() {
1427 ctx.draw_text_weighted(&shown, (x, y), size, weight, text_color);
1428 } else {
1429 let before: String = chars[..comp_start.min(chars.len())].iter().collect();
1430 let after: String = chars[comp_end.min(chars.len())..].iter().collect();
1431 let composed = format!("{before}{preedit}{after}");
1432 ctx.draw_text_weighted(&composed, (x, y), size, weight, text_color);
1433
1434 let stored = self.stored_bounds();
1435 let x0 = stored.get(comp_start).copied().unwrap_or(0.0);
1436 let pw = ctx.text_width_weighted(&preedit, size, weight);
1437 let line_y =
1438 underline::line_rect_padded(rect, size, line_w, line_width, ipx, ipy).top();
1439 ctx.quad(
1440 Rect::from_xywh(x + x0, line_y - 2.0, pw.max(1.0), 1.5),
1441 text_color,
1442 0.0,
1443 );
1444 }
1445
1446 let caret_rect = ctx
1447 .state
1448 .focused
1449 .then(|| Rect::from_xywh(x + caret_x, y - size * 0.8, CARET_W, size * 1.05))
1450 .filter(|_| !self.disabled);
1451 self.with_state(|s| s.caret_rect = caret_rect);
1452 if let Some(r) = caret_rect {
1453 ctx.quad(r, text_color, 0.0);
1454 }
1455
1456 ctx.pop_clip();
1457 }
1458}
1459
1460fn sanitize_margin(v: f32) -> f32 {
1461 if v.is_finite() { v.max(0.0) } else { 0.0 }
1462}