1use crate::*;
2use makepad_draw::text::selection::Cursor;
3use unicode_segmentation::UnicodeSegmentation;
4
5live_design! {
6 link widgets;
7 use link::widgets::*;
8 use link::theme::*;
9 use link::shaders::*;
10
11 List = {{List}} {
12 flow: Down,
13 width: Fill,
14 height: Fill,
15 }
16
17 pub CommandTextInput = {{CommandTextInput}} {
18 flow: Down,
19 height: Fit,
20
21 popup = <RoundedView> {
22 flow: Down,
23 height: Fit,
24 visible: false,
25
26 draw_bg: {
27 color: (THEME_COLOR_FG_APP),
28 border_size: (THEME_BEVELING),
29 border_color: (THEME_COLOR_BEVEL),
30 border_radius: (THEME_CORNER_RADIUS)
31
32 fn pixel(self) -> vec4 {
33 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
34
35 sdf.box_all(
37 0.0,
38 0.0,
39 self.rect_size.x,
40 self.rect_size.y,
41 self.border_radius,
42 self.border_radius,
43 self.border_radius,
44 self.border_radius
45 );
46 sdf.fill(self.border_color); sdf.box_all(
50 self.border_size,
51 self.border_size,
52 self.rect_size.x - self.border_size * 2.0,
53 self.rect_size.y - self.border_size * 2.0,
54 self.border_radius - self.border_size,
55 self.border_radius - self.border_size,
56 self.border_radius - self.border_size,
57 self.border_radius - self.border_size
58 );
59 sdf.fill(self.color); return sdf.result;
62 }
63 }
64
65 header_view = <View> {
66 width: Fill,
67 height: Fit,
68 padding: {left: 12., right: 12., top: 12., bottom: 12.}
69 show_bg: true
70 visible: true,
71 draw_bg: {
72 color: (THEME_COLOR_FG_APP),
73 instance top_radius: (THEME_CORNER_RADIUS),
74 instance border_color: (THEME_COLOR_BEVEL),
75 instance border_width: (THEME_BEVELING)
76 fn pixel(self) -> vec4 {
77 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
78 sdf.box_all(
79 0.0,
80 0.0,
81 self.rect_size.x,
82 self.rect_size.y,
83 self.top_radius,
84 self.top_radius,
85 1.0,
86 1.0
87 );
88 sdf.fill(self.color);
89 return sdf.result
90 }
91 }
92
93 header_label = <Label> {
94 draw_text: {
95 color: (THEME_COLOR_LABEL_INNER)
96 text_style: {
97 font_size: (THEME_FONT_SIZE_4)
98 }
99 }
100 }
101 }
102
103
104 search_input_wrapper = <RoundedView> {
107 height: Fit,
108 search_input = <TextInput> {
109 width: Fill,
110 height: Fit,
111 }
112 }
113
114 list = <List> {
115 height: Fit
116 }
117 }
118
119 persistent = <RoundedView> {
120 flow: Down,
121 height: Fit,
122 top = <View> { height: Fit }
123 center = <RoundedView> {
124 height: Fit,
125 left = <View> { width: Fit, height: Fit }
127 text_input = <TextInput> { width: Fill }
128 right = <View> { width: Fit, height: Fit }
129 }
130 bottom = <View> { height: Fit }
131 }
132 }
133}
134
135#[derive(Debug, Copy, Clone, DefaultNone)]
136enum InternalAction {
137 ShouldBuildItems,
138 ItemSelected,
139 None,
140}
141
142#[derive(Widget, Live)]
147pub struct CommandTextInput {
148 #[deref]
149 deref: View,
150
151 #[live]
158 pub trigger: Option<String>,
159
160 #[live]
165 pub inline_search: bool,
166
167 #[live]
169 pub color_focus: Vec4,
170
171 #[live]
173 pub color_hover: Vec4,
174
175 #[rust]
177 is_search_input_focus_pending: bool,
178
179 #[rust]
181 is_text_input_focus_pending: bool,
182
183 #[rust]
186 keyboard_focus_index: Option<usize>,
187
188 #[rust]
191 pointer_hover_index: Option<usize>,
192
193 #[rust]
195 selectable_widgets: Vec<WidgetRef>,
196
197 #[rust]
199 last_selected_widget: WidgetRef,
200
201 #[rust]
203 trigger_position: Option<usize>,
204
205 #[rust]
207 prev_cursor_position: usize,
208}
209
210impl Widget for CommandTextInput {
211 fn set_text(&mut self, cx: &mut Cx, v: &str) {
212 self.text_input_ref().set_text(cx, v);
213 }
214
215 fn text(&self) -> String {
216 self.text_input_ref().text()
217 }
218
219 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
220 self.update_highlights(cx);
221 self.ensure_popup_consistent(cx);
222
223 while !self.deref.draw_walk(cx, scope, walk).is_done() {}
224
225 if self.is_search_input_focus_pending {
226 self.is_search_input_focus_pending = false;
227 self.search_input_ref().set_key_focus(cx);
228 }
229
230 if self.is_text_input_focus_pending {
231 self.is_text_input_focus_pending = false;
232 self.text_input_ref().set_key_focus(cx);
233 }
234
235 DrawStep::done()
236 }
237
238 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
239 if cx.has_key_focus(self.key_controller_text_input_ref().area()) {
240 if let Event::KeyDown(key_event) = event {
241 let popup_visible = self.view(id!(popup)).visible();
242
243 if popup_visible {
244 let mut eat_the_event = true;
245
246 match key_event.key_code {
247 KeyCode::ArrowDown => {
248 self.pointer_hover_index = None;
250 self.on_keyboard_move(cx, 1);
251 },
252 KeyCode::ArrowUp => {
253 self.pointer_hover_index = None;
255 self.on_keyboard_move(cx, -1);
256 },
257 KeyCode::ReturnKey => {
258 self.on_keyboard_controller_input_submit(cx, scope);
259 }
260 KeyCode::Escape => {
261 self.is_text_input_focus_pending = true;
262 self.hide_popup(cx);
263 self.redraw(cx);
264 }
265 _ => {
266 eat_the_event = false;
267 }
268 };
269
270 if eat_the_event {
271 return;
272 }
273 }
274 }
275 }
276
277 self.deref.handle_event(cx, event, scope);
278
279 if cx.has_key_focus(self.text_input_ref().area()) {
280 if let Event::TextInput(input_event) = event {
281 self.on_text_inserted(cx, scope, &input_event.input);
282 }
283
284 if self.inline_search {
285 if let Some(trigger_pos) = self.trigger_position {
286 let current_pos = get_head(&self.text_input_ref());
287 let current_search = self.search_text();
288
289 if current_pos < trigger_pos || graphemes(¤t_search).any(is_whitespace) {
290 self.hide_popup(cx);
291 self.redraw(cx);
292 } else if self.prev_cursor_position != current_pos {
293 cx.widget_action(
295 self.widget_uid(),
296 &scope.path,
297 InternalAction::ShouldBuildItems,
298 );
299 self.ensure_popup_consistent(cx);
300 }
301 }
302 }
303 }
304
305 if let Event::Actions(actions) = event {
306 let mut selected_by_click = None;
307 let mut should_redraw = false;
308
309 for (idx, item) in self.selectable_widgets.iter().enumerate() {
310 let item = item.as_view();
311
312 if item
313 .finger_down(actions)
314 .map(|fe| fe.tap_count == 1)
315 .unwrap_or(false)
316 {
317 selected_by_click = Some((&*item).clone());
318
319 self.keyboard_focus_index = None;
321 }
322
323 if item.finger_hover_out(actions).is_some() && Some(idx) == self.pointer_hover_index
324 {
325 self.pointer_hover_index = None;
326 should_redraw = true;
327 }
328
329 if item.finger_hover_in(actions).is_some() {
330 self.pointer_hover_index = Some(idx);
332 self.keyboard_focus_index = None;
333 should_redraw = true;
334 }
335 }
336
337 if should_redraw {
338 self.redraw(cx);
339 }
340
341 if let Some(selected) = selected_by_click {
342 self.select_item(cx, scope, selected);
343 }
344
345 for action in actions.iter().filter_map(|a| a.as_widget_action()) {
346 if action.widget_uid == self.key_controller_text_input_ref().widget_uid() {
347 if let TextInputAction::KeyFocusLost = action.cast() {
348 self.hide_popup(cx);
349 self.redraw(cx);
350 }
351 }
352
353 if action.widget_uid == self.search_input_ref().widget_uid() {
354 if let TextInputAction::Changed(search) = action.cast() {
355 self.search_input_ref()
357 .set_text(cx, search.lines().next().unwrap_or_default());
358
359 cx.widget_action(
360 self.widget_uid(),
361 &scope.path,
362 InternalAction::ShouldBuildItems,
363 );
364 self.ensure_popup_consistent(cx);
365 }
366 }
367 }
368 }
369
370 self.prev_cursor_position = get_head(&self.text_input_ref());
371 self.ensure_popup_consistent(cx);
372 }
373}
374
375impl CommandTextInput {
376 fn ensure_popup_consistent(&mut self, cx: &mut Cx) {
378 if self.view(id!(popup)).visible() {
379 if self.inline_search {
380 self.view(id!(search_input_wrapper)).set_visible(cx, false);
381 } else {
382 self.view(id!(search_input_wrapper)).set_visible(cx, true);
383 }
384 }
385 }
386
387 pub fn keyboard_focus_index(&self) -> Option<usize> {
388 self.keyboard_focus_index
389 }
390
391 pub fn set_keyboard_focus_index(&mut self, idx: usize) {
394 if !self.selectable_widgets.is_empty() {
396 self.keyboard_focus_index = Some(idx.clamp(0, self.selectable_widgets.len() - 1));
398 }
399 }
400
401 fn on_text_inserted(&mut self, cx: &mut Cx, scope: &mut Scope, inserted: &str) {
402 if graphemes(inserted).last() == self.trigger_grapheme() {
403 self.show_popup(cx);
404 self.trigger_position = Some(get_head(&self.text_input_ref()));
405
406 if self.inline_search {
407 self.view(id!(search_input_wrapper)).set_visible(cx, false);
408 } else {
409 self.view(id!(search_input_wrapper)).set_visible(cx, true);
410 self.is_search_input_focus_pending = true;
411 }
412
413 cx.widget_action(
414 self.widget_uid(),
415 &scope.path,
416 InternalAction::ShouldBuildItems,
417 );
418 self.ensure_popup_consistent(cx);
419 }
420 }
421
422 fn on_keyboard_controller_input_submit(&mut self, cx: &mut Cx, scope: &mut Scope) {
423 let Some(idx) = self.keyboard_focus_index else {
424 return;
425 };
426
427 self.select_item(cx, scope, self.selectable_widgets[idx].clone());
428 }
429
430 fn select_item(&mut self, cx: &mut Cx, scope: &mut Scope, selected: WidgetRef) {
431 self.try_remove_trigger_and_inline_search(cx);
432 self.last_selected_widget = selected;
433 cx.widget_action(self.widget_uid(), &scope.path, InternalAction::ItemSelected);
434 self.hide_popup(cx);
435 self.is_text_input_focus_pending = true;
436 self.redraw(cx);
437 }
438
439 fn try_remove_trigger_and_inline_search(&mut self, cx: &mut Cx) {
440 let mut to_remove = self.trigger_grapheme().unwrap_or_default().to_string();
441
442 if self.inline_search {
443 to_remove.push_str(&self.search_text());
444 }
445
446 let text = self.text();
447 let end = get_head(&self.text_input_ref());
448 let text_graphemes: Vec<&str> = text.graphemes(true).collect();
450 let mut byte_index = 0;
451 let mut end_grapheme_idx = 0;
452
453 for (i, g) in text_graphemes.iter().enumerate() {
455 if byte_index <= end && byte_index + g.len() > end {
456 end_grapheme_idx = i;
457 break;
458 }
459 byte_index += g.len();
460 }
461
462 let start_grapheme_idx = if end_grapheme_idx >= to_remove.graphemes(true).count() {
464 end_grapheme_idx - to_remove.graphemes(true).count()
465 } else {
466 return;
467 };
468
469 let new_text = text_graphemes[..start_grapheme_idx].join("") +
471 &text_graphemes[end_grapheme_idx..].join("");
472
473 let new_cursor_pos = text_graphemes[..start_grapheme_idx].join("").graphemes(true).count();
475
476 self.text_input_ref().set_cursor(
477 cx,
478 Cursor {
479 index: new_cursor_pos,
480 prefer_next_row: false,
481 },
482 false
483 );
484 self.set_text(cx, &new_text);
485 }
486
487 fn show_popup(&mut self, cx: &mut Cx) {
488 if self.inline_search {
489 self.view(id!(search_input_wrapper)).set_visible(cx, false);
490 } else {
491 self.view(id!(search_input_wrapper)).set_visible(cx, true);
492 }
493 self.view(id!(popup)).set_visible(cx, true);
494 self.view(id!(popup)).redraw(cx);
495 }
496
497 fn hide_popup(&mut self, cx: &mut Cx) {
498 self.clear_popup(cx);
499 self.view(id!(popup)).set_visible(cx, false);
500 }
501
502 pub fn reset(&mut self, cx: &mut Cx) {
504 self.hide_popup(cx);
505 self.text_input_ref().set_text(cx, "");
506 }
507
508 fn clear_popup(&mut self, cx: &mut Cx) {
509 self.trigger_position = None;
510 self.search_input_ref().set_text(cx, "");
511 self.search_input_ref().set_cursor(
512 cx,
513 Cursor {
514 index: 0,
515 prefer_next_row: false,
516 },
517 false
518 );
519 self.clear_items();
520 }
521
522 pub fn clear_items(&mut self) {
526 self.list(id!(list)).clear();
527 self.selectable_widgets.clear();
528 self.keyboard_focus_index = None;
529 self.pointer_hover_index = None;
530 }
531
532 pub fn add_item(&mut self, widget: WidgetRef) {
536 self.list(id!(list)).add(widget.clone());
537 self.selectable_widgets.push(widget);
538 self.keyboard_focus_index = self.keyboard_focus_index.or(Some(0));
539 }
540
541 pub fn add_unselectable_item(&mut self, widget: WidgetRef) {
547 self.list(id!(list)).add(widget);
548 }
549
550 pub fn search_text(&self) -> String {
554 const MAX_SEARCH_TEXT_LENGTH: usize = 100;
556
557 if self.inline_search {
558 if let Some(trigger_pos) = self.trigger_position {
559 let text = self.text();
560 let head = get_head(&self.text_input_ref());
561
562 if head > trigger_pos {
563 let text_graphemes: Vec<&str> = text.graphemes(true).collect();
565 let mut byte_pos = 0;
566 let mut trigger_grapheme_idx = None;
567 let mut head_grapheme_idx = None;
568 let mut last_grapheme_end = 0;
569
570 for (i, g) in text_graphemes.iter().enumerate() {
572 if byte_pos <= trigger_pos && byte_pos + g.len() > trigger_pos {
574 trigger_grapheme_idx = Some(i);
575 }
576 else if byte_pos + g.len() == trigger_pos {
578 trigger_grapheme_idx = Some(i + 1);
580 }
581
582 if byte_pos <= head && byte_pos + g.len() > head {
584 head_grapheme_idx = Some(i);
585 }
586 else if byte_pos + g.len() == head {
588 head_grapheme_idx = Some(i + 1);
590 }
591
592 byte_pos += g.len();
593 last_grapheme_end = byte_pos;
594 }
595
596 if head_grapheme_idx.is_none() && head >= last_grapheme_end {
598 head_grapheme_idx = Some(text_graphemes.len());
599 }
600
601 if trigger_grapheme_idx.is_none() && trigger_pos >= last_grapheme_end {
602 trigger_grapheme_idx = Some(text_graphemes.len());
603 }
604
605 if let (Some(t_idx), Some(h_idx)) = (trigger_grapheme_idx, head_grapheme_idx) {
607 if t_idx >= text_graphemes.len() || h_idx > text_graphemes.len() {
609 log!("Error: Grapheme indices out of range: t_idx={}, h_idx={}, graphemes_len={}",
610 t_idx, h_idx, text_graphemes.len());
611 return String::new();
612 }
613
614 if t_idx < h_idx {
615 let length = h_idx - t_idx;
617 if length > MAX_SEARCH_TEXT_LENGTH {
618 log!("Warning: Search text length({}) exceeds maximum limit({})", length, MAX_SEARCH_TEXT_LENGTH);
619 return text_graphemes[t_idx..t_idx + MAX_SEARCH_TEXT_LENGTH].join("");
621 }
622
623 let mut result = String::with_capacity(
625 text_graphemes[t_idx..h_idx].iter().map(|g| g.len()).sum()
626 );
627 for g in &text_graphemes[t_idx..h_idx] {
628 result.push_str(g);
629 }
630 return result;
631 } else if t_idx == h_idx {
632 return String::new();
634 } else {
635 log!("Warning: Trigger character is after cursor: trigger_idx={}, head_idx={}, trigger_pos={}, head={}",
637 t_idx, h_idx, trigger_pos, head);
638 return String::new();
639 }
640 } else {
641 log!("Warning: Unable to find valid grapheme indices: trigger_idx={:?}, head_idx={:?}, trigger_pos={}, head={}, text_len={}, graphemes_len={}",
643 trigger_grapheme_idx, head_grapheme_idx, trigger_pos, head, text.len(), text_graphemes.len());
644 return String::new();
645 }
646 }
647
648 String::new()
650 } else {
651 String::new()
653 }
654 } else {
655 self.search_input_ref().text()
657 }
658 }
659
660 pub fn item_selected(&self, actions: &Actions) -> Option<WidgetRef> {
663 actions
664 .iter()
665 .filter_map(|a| a.as_widget_action())
666 .filter(|a| a.widget_uid == self.widget_uid())
667 .find_map(|a| {
668 if let InternalAction::ItemSelected = a.cast() {
669 Some(self.last_selected_widget.clone())
670 } else {
671 None
672 }
673 })
674 }
675
676 pub fn should_build_items(&self, actions: &Actions) -> bool {
682 actions
683 .iter()
684 .filter_map(|a| a.as_widget_action())
685 .filter(|a| a.widget_uid == self.widget_uid())
686 .any(|a| matches!(a.cast(), InternalAction::ShouldBuildItems))
687 }
688
689 pub fn text_input_ref(&self) -> TextInputRef {
691 self.text_input(id!(text_input))
692 }
693
694 pub fn search_input_ref(&self) -> TextInputRef {
696 self.text_input(id!(search_input))
697 }
698
699 fn trigger_grapheme(&self) -> Option<&str> {
700 self.trigger.as_ref().and_then(|t| graphemes(t).next())
701 }
702
703 fn key_controller_text_input_ref(&self) -> TextInputRef {
704 if self.inline_search {
705 self.text_input_ref()
706 } else {
707 self.search_input_ref()
708 }
709 }
710
711 fn on_keyboard_move(&mut self, cx: &mut Cx, delta: i32) {
712 let Some(idx) = self.keyboard_focus_index else {
713 if !self.selectable_widgets.is_empty() {
715 if delta > 0 {
716 self.keyboard_focus_index = Some(0);
717 } else {
718 self.keyboard_focus_index = Some(self.selectable_widgets.len() - 1);
719 }
720 }
721 return;
722 };
723
724 let new_index = idx
725 .saturating_add_signed(delta as isize)
726 .clamp(0, self.selectable_widgets.len() - 1);
727
728 if idx != new_index {
729 self.keyboard_focus_index = Some(new_index);
730 }
731
732 self.pointer_hover_index = None;
735
736 self.redraw(cx);
737 }
738
739 fn update_highlights(&mut self, cx: &mut Cx) {
740 let has_keyboard_focus = self.keyboard_focus_index.is_some();
742
743 for (idx, item) in self.selectable_widgets.iter().enumerate() {
744 item.apply_over(cx, live! { show_bg: true, cursor: Hand });
745
746 if Some(idx) == self.keyboard_focus_index {
749 item.apply_over(
751 cx,
752 live! {
753 draw_bg: {
754 color: (self.color_focus),
755 }
756 },
757 );
758 } else if Some(idx) == self.pointer_hover_index && !has_keyboard_focus {
759 item.apply_over(
761 cx,
762 live! {
763 draw_bg: {
764 color: (self.color_hover),
765 }
766 },
767 );
768 } else {
769 item.apply_over(
771 cx,
772 live! {
773 draw_bg: {
774 color: (Vec4::all(0.)),
775 }
776 },
777 );
778 }
779 }
780 }
781
782 pub fn request_text_input_focus(&mut self) {
784 self.is_text_input_focus_pending = true;
785 }
786}
787
788impl LiveHook for CommandTextInput {}
789
790impl CommandTextInputRef {
791 pub fn should_build_items(&self, actions: &Actions) -> bool {
793 self.borrow()
794 .map_or(false, |inner| inner.should_build_items(actions))
795 }
796
797 pub fn clear_items(&mut self) {
799 if let Some(mut inner) = self.borrow_mut() {
800 inner.clear_items();
801 }
802 }
803
804 pub fn add_item(&self, widget: WidgetRef) {
806 if let Some(mut inner) = self.borrow_mut() {
807 inner.add_item(widget);
808 }
809 }
810
811 pub fn add_unselectable_item(&self, widget: WidgetRef) {
813 if let Some(mut inner) = self.borrow_mut() {
814 inner.add_unselectable_item(widget);
815 }
816 }
817
818 pub fn item_selected(&self, actions: &Actions) -> Option<WidgetRef> {
820 self.borrow().and_then(|inner| inner.item_selected(actions))
821 }
822
823 pub fn text_input_ref(&self) -> TextInputRef {
825 self.borrow()
826 .map_or(WidgetRef::empty().as_text_input(), |inner| {
827 inner.text_input_ref()
828 })
829 }
830
831 pub fn search_input_ref(&self) -> TextInputRef {
833 self.borrow()
834 .map_or(WidgetRef::empty().as_text_input(), |inner| {
835 inner.search_input_ref()
836 })
837 }
838
839 pub fn reset(&self, cx: &mut Cx) {
841 if let Some(mut inner) = self.borrow_mut() {
842 inner.reset(cx);
843 }
844 }
845
846 pub fn request_text_input_focus(&self) {
848 if let Some(mut inner) = self.borrow_mut() {
849 inner.request_text_input_focus();
850 }
851 }
852
853 pub fn search_text(&self) -> String {
855 self.borrow()
856 .map_or(String::new(), |inner| inner.search_text())
857 }
858}
859
860fn graphemes(text: &str) -> impl DoubleEndedIterator<Item = &str> {
861 text.graphemes(true)
862}
863
864fn get_head(text_input: &TextInputRef) -> usize {
865 text_input.borrow().map_or(0, |p| p.cursor().index)
866}
867
868fn is_whitespace(grapheme: &str) -> bool {
869 grapheme.chars().all(char::is_whitespace)
870}
871
872#[derive(Live, Widget, LiveHook)]
874struct List {
875 #[walk]
876 walk: Walk,
877
878 #[layout]
879 layout: Layout,
880
881 #[redraw]
882 #[rust]
883 area: Area,
884
885 #[rust]
886 items: Vec<WidgetRef>,
887}
888
889impl Widget for List {
890 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
891 self.items.iter().for_each(|item| {
892 item.handle_event(cx, event, scope);
893 });
894 }
895
896 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
897 cx.begin_turtle(walk, self.layout);
898 self.items.iter().for_each(|item| {
899 item.draw_all(cx, scope);
900 });
901 cx.end_turtle_with_area(&mut self.area);
902 DrawStep::done()
903 }
904}
905
906impl List {
907 fn clear(&mut self) {
908 self.items.clear();
909 }
910
911 fn add(&mut self, widget: WidgetRef) {
912 self.items.push(widget);
913 }
914}
915
916impl ListRef {
917 fn clear(&self) {
918 let Some(mut inner) = self.borrow_mut() else {
919 return;
920 };
921
922 inner.clear();
923 }
924
925 fn add(&self, widget: WidgetRef) {
926 let Some(mut inner) = self.borrow_mut() else {
927 return;
928 };
929
930 inner.add(widget);
931 }
932}