makepad_widgets/
command_text_input.rs

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                    // External outline (entire component including border)
36                    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);  // Fill the entire area with border color
47
48                    // Internal outline (content area)
49                    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);  // Fill content area with background color
60
61                    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            // Wrapper workaround to hide search input when inline search is enabled
105            // as we currently can't hide the search input avoiding events.
106            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` and `right` seems to not work with `height: Fill`.
126                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/// `TextInput` wrapper glued to a popup list of options that is shown when a
143/// trigger character is typed.
144///
145/// Limitation: Selectable items are expected to be `View`s.
146#[derive(Widget, Live)]
147pub struct CommandTextInput {
148    #[deref]
149    deref: View,
150
151    /// The character that triggers the popup.
152    ///
153    /// If not set, popup can't be triggerd by keyboard.
154    ///
155    /// Behavior is undefined if this string contains anything other than a
156    /// single grapheme.
157    #[live]
158    pub trigger: Option<String>,
159
160    /// Handle search within the main text input instead of using a separate
161    /// search input.
162    ///
163    /// Note: Any kind of whitespace will terminate search.
164    #[live]
165    pub inline_search: bool,
166
167    /// Strong color to highlight the item that would be submitted if `Return` is pressed.
168    #[live]
169    pub color_focus: Vec4,
170
171    /// Weak color to highlight the item that the pointer is hovering over.
172    #[live]
173    pub color_hover: Vec4,
174
175    /// To deal with focus requesting issues.
176    #[rust]
177    is_search_input_focus_pending: bool,
178
179    /// To deal with focus requesting issues.
180    #[rust]
181    is_text_input_focus_pending: bool,
182
183    /// Index from `selectable_widgets` that would be submitted if `Return` is pressed.
184    /// `None` if there are no selectable widgets.
185    #[rust]
186    keyboard_focus_index: Option<usize>,
187
188    /// Index from `selectable_widgets` that the pointer is hovering over.
189    /// `None` if there are no selectable widgets.
190    #[rust]
191    pointer_hover_index: Option<usize>,
192
193    /// Convenience copy of the selectable widgets on the popup list.
194    #[rust]
195    selectable_widgets: Vec<WidgetRef>,
196
197    /// To deal with widgets not being `Send`.
198    #[rust]
199    last_selected_widget: WidgetRef,
200
201    /// Remember where trigger was inserted to support `inline_search`.
202    #[rust]
203    trigger_position: Option<usize>,
204
205    /// Remmeber which was the last cursor position handled, to support `inline_search`.
206    #[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                            // Clear mouse hover when using up/down keys
249                            self.pointer_hover_index = None;
250                            self.on_keyboard_move(cx, 1);
251                        },
252                        KeyCode::ArrowUp => {
253                            // Clear mouse hover when using up/down keys
254                            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(&current_search).any(is_whitespace) {
290                        self.hide_popup(cx);
291                        self.redraw(cx);
292                    } else if self.prev_cursor_position != current_pos {
293                        // mimic how discord updates the filter when moving the cursor
294                        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                    // Clear keyboard focus when mouse is clicked
320                    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                    // When mouse enters item, clear keyboard focus and set mouse hover index
331                    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                        // disallow multiline input
356                        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    // Ensure popup state consistency
377    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    /// Sets the keyboard focus index for the list of selectable items
392    /// Only updates the visual highlight state of the dropdown items
393    pub fn set_keyboard_focus_index(&mut self, idx: usize) {
394        // Only process if popup is visible and we have items
395        if !self.selectable_widgets.is_empty() {
396            // Simply update the focus index within valid bounds
397            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        // Use graphemes instead of byte indices
449        let text_graphemes: Vec<&str> = text.graphemes(true).collect();
450        let mut byte_index = 0;
451        let mut end_grapheme_idx = 0;
452
453        // Find the grapheme index corresponding to the end position
454        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        // Calculate the start grapheme index
463        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        // Rebuild the string
470        let new_text = text_graphemes[..start_grapheme_idx].join("") +
471                        &text_graphemes[end_grapheme_idx..].join("");
472
473        // Calculate the new cursor position (grapheme)
474        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    /// Clear all text and hide the popup going back to initial state.
503    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    /// Clears the list of items.
523    ///
524    /// Normally called as response to `should_build_items`.
525    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    /// Add a custom selectable item to the list.
533    ///
534    /// Normally called after clearing the previous items.
535    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    /// Add a custom unselectable item to the list.
542    ///
543    /// Ex: Headers, dividers, etc.
544    ///
545    /// Normally called after clearing the previous items.
546    pub fn add_unselectable_item(&mut self, widget: WidgetRef) {
547        self.list(id!(list)).add(widget);
548    }
549
550    /// Get the current search query.
551    ///
552    /// You probably want this for filtering purposes when updating the items.
553    pub fn search_text(&self) -> String {
554        // Define maximum search text length to prevent performance issues with very long search texts
555        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                    // Parse text into graphemes (Unicode grapheme clusters)
564                    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                    // Single-pass traversal to calculate all grapheme indices
571                    for (i, g) in text_graphemes.iter().enumerate() {
572                        // Check if the trigger character is within this grapheme
573                        if byte_pos <= trigger_pos && byte_pos + g.len() > trigger_pos {
574                            trigger_grapheme_idx = Some(i);
575                        }
576                        // Check if the trigger character is exactly at the end of this grapheme
577                        else if byte_pos + g.len() == trigger_pos {
578                            // Special case: trigger at grapheme boundary, point to the next grapheme
579                            trigger_grapheme_idx = Some(i + 1);
580                        }
581
582                        // Check if the cursor is within this grapheme
583                        if byte_pos <= head && byte_pos + g.len() > head {
584                            head_grapheme_idx = Some(i);
585                        }
586                        // Check if the cursor is exactly at the end of this grapheme
587                        else if byte_pos + g.len() == head {
588                            // Special case: cursor at grapheme boundary, point to the next grapheme
589                            head_grapheme_idx = Some(i + 1);
590                        }
591
592                        byte_pos += g.len();
593                        last_grapheme_end = byte_pos;
594                    }
595
596                    // Handle edge cases at the end of text symmetrically for both positions
597                    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                    // Safety check and use indices only if they're valid
606                    if let (Some(t_idx), Some(h_idx)) = (trigger_grapheme_idx, head_grapheme_idx) {
607                        // Additional range check to prevent index errors
608                        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                            // Check length limit
616                            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                                // Still return text but truncated to the maximum length
620                                return text_graphemes[t_idx..t_idx + MAX_SEARCH_TEXT_LENGTH].join("");
621                            }
622
623                            // Optimized string building with pre-allocated capacity
624                            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                            // Edge case: trigger character and cursor in the same grapheme
633                            return String::new();
634                        } else {
635                            // Abnormal case: trigger character is after the cursor
636                            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                        // Comprehensive diagnostic information
642                        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                // Cursor is at or before the trigger position
649                String::new()
650            } else {
651                // No trigger position
652                String::new()
653            }
654        } else {
655            // Non-inline search mode
656            self.search_input_ref().text()
657        }
658    }
659
660    /// Checks if any item has been selected in the given `actions`
661    /// and returns a reference to the selected item as a widget.
662    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    /// Returns `true` if an action in the given `actions` indicates that
677    /// the items to display need to be recomputed again.
678    ///
679    /// For example, this returns true if the trigger character was typed,
680    /// if the search filter changes, etc.
681    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    /// Returns a reference to the inner `TextInput` widget.
690    pub fn text_input_ref(&self) -> TextInputRef {
691        self.text_input(id!(text_input))
692    }
693
694    /// Returns a reference to the inner `TextInput` widget used for search.
695    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 no keyboard focus exists but user pressed arrow keys, focus on first item
714            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        // Clear mouse hover state when using keyboard navigation
733        // This ensures keyboard navigation and mouse hover don't appear simultaneously
734        self.pointer_hover_index = None;
735
736        self.redraw(cx);
737    }
738
739    fn update_highlights(&mut self, cx: &mut Cx) {
740        // Check if currently there is a keyboard-focused item
741        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 there is a keyboard focus, prioritize it over mouse hover
747            // If there is no keyboard focus, show mouse hover
748            if Some(idx) == self.keyboard_focus_index {
749                // Keyboard-selected item is highlighted in blue
750                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                // Mouse-hovered item is highlighted in gray, but only when there is no keyboard focus
760                item.apply_over(
761                    cx,
762                    live! {
763                        draw_bg: {
764                            color: (self.color_hover),
765                        }
766                    },
767                );
768            } else {
769                // Default state
770                item.apply_over(
771                    cx,
772                    live! {
773                        draw_bg: {
774                            color: (Vec4::all(0.)),
775                        }
776                    },
777                );
778            }
779        }
780    }
781
782    /// Obtain focus in the main `TextInput` widget as soon as possible.
783    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    /// See [`CommandTextInput::should_build_items()`].
792    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    /// See [`CommandTextInput::clear_items()`].
798    pub fn clear_items(&mut self) {
799        if let Some(mut inner) = self.borrow_mut() {
800            inner.clear_items();
801        }
802    }
803
804    /// See [`CommandTextInput::add_item()`].
805    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    /// See [`CommandTextInput::add_unselectable_item()`].
812    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    /// See [`CommandTextInput::item_selected()`].
819    pub fn item_selected(&self, actions: &Actions) -> Option<WidgetRef> {
820        self.borrow().and_then(|inner| inner.item_selected(actions))
821    }
822
823    /// See [`CommandTextInput::text_input_ref()`].
824    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    /// See [`CommandTextInput::search_input_ref()`].
832    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    /// See [`CommandTextInput::reset()`].
840    pub fn reset(&self, cx: &mut Cx) {
841        if let Some(mut inner) = self.borrow_mut() {
842            inner.reset(cx);
843        }
844    }
845
846    /// See [`CommandTextInput::request_text_input_focus()`].
847    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    /// See [`CommandTextInput::search_text()`].
854    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/// Reduced and adapted copy of the `List` widget from Moly.
873#[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}