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