Skip to main content

ferroui_controls/
combo_box.rs

1use crate::assigned_binding::AssignedBinding;
2use crate::generators::RecycleKey;
3use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
4use crate::primitives::{
5    ItemSelectionEventTriggers, Popup, SelectingItemsControl, SelectingItemsControlImpl, SelectingItemsControlImplExt,
6    TemplateAppliedEventArgs, TemplatedControlImpl, TextSearch,
7};
8use crate::shapes::Rectangle;
9use crate::templates::{FuncTemplate, IDataTemplate, ITemplateOf};
10use crate::utils::BindingEvaluator;
11use crate::{
12    ComboBoxItem, ContentControl, Control, ControlImpl, ItemsControl, ItemsControlImpl, Panel, TextBlock, TextBox,
13    VirtualizingStackPanel,
14};
15use ferroui_base::data::core::ValueTypes;
16use ferroui_base::data::BindingMode;
17use ferroui_base::input::navigation::XYFocusHelpers;
18use ferroui_base::input::{
19    FocusChangedEventArgs, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyModifiers, KeyboardNavigation,
20    NavigationMethod, PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs, PointerUpdateKind,
21    PointerWheelEventArgs,
22};
23use ferroui_base::interactivity::{IRoutedEventArgs, InteractiveImpl};
24use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, VerticalAlignment};
25use ferroui_base::media::{AlignmentX, FlowDirection, IBrush, Stretch, VisualBrush};
26use ferroui_base::reactive::{CompositeDisposable, Disposable, IDisposable, ObservableExt};
27use ferroui_base::utilities::HandlerList;
28use ferroui_base::{
29    ferro_class, ferro_impl_classes, ferro_property, instantiate, BoxedValue, DirectProperty, FerroObject,
30    FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, Size,
31    StaticType, StyledElement, StyledElementImpl, StyledProperty, StyledPropertyMetadata, StyledPropertyOptions, Visual, VisualImpl,
32    VisualImplExt, VisualTreeAttachmentEventArgs,
33};
34use std::cell::{Cell, RefCell};
35use std::rc::Rc;
36
37/// Resets a flag when dropped.
38struct Reset<'a>(&'a Cell<bool>);
39
40impl Drop for Reset<'_> {
41    fn drop(&mut self) {
42        self.0.set(false);
43    }
44}
45
46/// A drop-down list control.
47#[repr(C)]
48pub struct ComboBox {
49    base: SelectingItemsControl,
50    popup: RefCell<Option<Ref<Popup>>>,
51    // The subscriptions to the opened and closed events of the popup.
52    popup_subscriptions: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
53    selection_box_item: RefCell<Option<BoxedValue>>,
54    subscriptions_on_open: CompositeDisposable,
55    input_text_box: RefCell<Option<Ref<TextBox>>>,
56    text_value_binding_evaluator: RefCell<Option<Ref<BindingEvaluator>>>,
57    skip_next_text_changed: Cell<bool>,
58    drop_down_closed: HandlerList<dyn Fn()>,
59    drop_down_opened: HandlerList<dyn Fn()>,
60}
61
62ferro_class!(ComboBox: SelectingItemsControl);
63ferroui_base::ferro_class_info!(ComboBox { new: ComboBox::new });
64ferro_impl_classes!(ComboBox: StyledElementImpl, LayoutableImpl, InteractiveImpl);
65
66impl ControlImpl for ComboBox {
67    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
68        crate::automation::peers::ComboBoxAutomationPeer::new(this).upcast()
69    }
70}
71
72impl FerroObjectImpl for ComboBox {
73    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
74        let property = change.property();
75
76        if property == SelectingItemsControl::selected_item_property().as_property() {
77            let new_value = change.get_new_value::<Option<BoxedValue>>();
78            this.update_selection_box_item(new_value.clone());
79            this.try_focus_selected_item();
80            this.update_input_text_from_selection(&new_value);
81        } else if property == Self::is_drop_down_open_property().as_property() {
82            this.pseudo_classes().set(Self::PC_DROPDOWN_OPEN, change.get_new_value::<bool>());
83        } else if property == ItemsControl::item_template_property().as_property() {
84            this.coerce_value(Self::selection_box_item_template_property().as_property());
85        } else if property == Self::selection_box_item_template_property().as_property() {
86            this.update_selection_box_item(this.selected_item());
87        } else if property == Self::is_editable_property().as_property() && change.get_new_value::<bool>() {
88            this.update_input_text_from_selection(&this.selected_item());
89        } else if property == Self::text_property().as_property() {
90            this.text_changed(change.get_new_value::<Option<String>>());
91        } else if property == ItemsControl::items_source_property().as_property() {
92            // The base handler deselects the current item (and resets the
93            // text) so we want to run the base first, then try to match by
94            // text.
95            let text = this.text();
96            Self::parent_on_property_changed(this, change);
97            this.set_current_value(Self::text_property(), text);
98            return;
99        } else if property == ItemsControl::display_member_binding_property().as_property() {
100            this.handle_text_value_binding_value_changed(None, Some(change));
101            // The base handler invalidates the cached template: run it
102            // before coercing.
103            Self::parent_on_property_changed(this, change);
104            this.coerce_value(Self::selection_box_item_template_property().as_property());
105            return;
106        } else if property == TextSearch::text_binding_property().as_property() {
107            this.handle_text_value_binding_value_changed(Some(change), None);
108        }
109
110        Self::parent_on_property_changed(this, change);
111    }
112}
113
114impl VisualImpl for ComboBox {
115    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
116        Self::parent_on_attached_to_visual_tree(this, e);
117        this.update_selection_box_item(this.selected_item());
118    }
119
120    fn invalidate_mirror_transform(this: &Self) {
121        Self::parent_invalidate_mirror_transform(this);
122        this.update_flow_direction();
123    }
124}
125
126impl InputElementImpl for ComboBox {
127    fn on_key_down(this: &Self, e: &KeyEventArgs) {
128        Self::parent_on_key_down(this, e);
129
130        if e.handled() {
131            return;
132        }
133
134        let alt = e.key_modifiers.contains(KeyModifiers::ALT);
135
136        if (e.key == Key::F4 && !alt) || ((e.key == Key::Down || e.key == Key::Up) && alt) {
137            this.set_current_value(Self::is_drop_down_open_property(), !this.is_drop_down_open());
138            e.set_handled(true);
139        } else if this.is_drop_down_open() && e.key == Key::Escape {
140            this.set_current_value(Self::is_drop_down_open_property(), false);
141            e.set_handled(true);
142        } else if !this.is_drop_down_open() && !this.is_editable() && (e.key == Key::Enter || e.key == Key::Space) {
143            this.set_current_value(Self::is_drop_down_open_property(), true);
144            e.set_handled(true);
145        } else if this.is_drop_down_open() && e.key == Key::Tab {
146            this.set_current_value(Self::is_drop_down_open_property(), false);
147        }
148        // Ignore key buttons, if they are used for XY focus.
149        else if !this.is_drop_down_open() && !XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type)) {
150            if e.key == Key::Down {
151                e.set_handled(this.select_next());
152            } else if e.key == Key::Up {
153                e.set_handled(this.select_previous());
154            }
155        }
156        // This part of code is needed just to acquire initial focus,
157        // subsequent focus navigation will be done by the items control.
158        else if this.is_drop_down_open()
159            && this.selected_index() < 0
160            && this.item_count() > 0
161            && (e.key == Key::Up || e.key == Key::Down)
162            && this.is_focused()
163        {
164            let first_child = this
165                .presenter()
166                .and_then(|presenter| presenter.panel())
167                .and_then(|panel| panel.children().snapshot().iter().find(|c| Self::can_focus(c)).cloned());
168            if let Some(first_child) = first_child {
169                e.set_handled(first_child.focus_with(NavigationMethod::Directional, KeyModifiers::NONE));
170            }
171        }
172    }
173
174    fn on_pointer_wheel_changed(this: &Self, e: &PointerWheelEventArgs) {
175        Self::parent_on_pointer_wheel_changed(this, e);
176
177        if !e.handled() {
178            if !this.is_drop_down_open() {
179                if this.is_focused() {
180                    e.set_handled(if e.delta().y < 0.0 { this.select_next() } else { this.select_previous() });
181                }
182            } else {
183                e.set_handled(true);
184            }
185        }
186    }
187
188    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
189        Self::parent_on_pointer_pressed(this, e);
190        if !e.handled() {
191            if let Some(source) = e.source().and_then(|source| source.cast::<Visual>()) {
192                if this.is_inside_popup(&source) {
193                    e.set_handled(true);
194                    return;
195                }
196            }
197        }
198
199        if this.is_drop_down_open() {
200            // When a drop-down is open with the pass through of the overlay
201            // dismiss event enabled and the control is pressed, close the
202            // drop-down.
203            this.set_current_value(Self::is_drop_down_open_property(), false);
204            e.set_handled(true);
205        } else {
206            this.pseudo_classes().set(Self::PC_PRESSED, true);
207        }
208    }
209
210    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
211        // If the user clicked in the input text we don't want to open the
212        // dropdown.
213        let input_text_box = this.input_text_box.borrow().clone();
214        if let Some(input_text_box) = input_text_box {
215            if !e.handled() {
216                let styled_source = e.source().and_then(|source| source.cast::<StyledElement>());
217                if let Some(styled_source) = styled_source {
218                    if styled_source.templated_parent().is_some_and(|parent| parent.ptr_eq(&input_text_box)) {
219                        return;
220                    }
221                }
222            }
223        }
224
225        if !e.handled() {
226            if let Some(source) = e.source().and_then(|source| source.cast::<Visual>()) {
227                if !this.is_inside_popup(&source) && this.pseudo_classes().contains(Self::PC_PRESSED) {
228                    this.set_current_value(Self::is_drop_down_open_property(), !this.is_drop_down_open());
229                    crate::platform::PlatformFeedbackExtensions::perform_feedback(
230                        &**this as &InputElement,
231                        crate::platform::FeedbackAction::click(),
232                    );
233                    e.set_handled(true);
234                }
235            }
236        }
237
238        this.pseudo_classes().set(Self::PC_PRESSED, false);
239        Self::parent_on_pointer_released(this, e);
240    }
241
242    fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
243        if this.is_editable() {
244            let input_text_box = this.input_text_box.borrow().clone();
245            if let Some(input_text_box) = input_text_box {
246                input_text_box.focus();
247                input_text_box.select_all();
248            }
249        }
250
251        Self::parent_on_got_focus(this, e);
252    }
253}
254
255impl TemplatedControlImpl for ComboBox {
256    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
257        if let Some((opened, closed)) = this.popup_subscriptions.take() {
258            opened.dispose();
259            closed.dispose();
260        }
261
262        let popup = e.name_scope().get_as::<Popup>("PART_Popup");
263
264        let weak = this.to_ref().downgrade();
265        let opened = popup.opened(move || {
266            if let Some(this) = weak.upgrade() {
267                this.popup_opened();
268            }
269        });
270        let weak = this.to_ref().downgrade();
271        let closed = popup.closed(move || {
272            if let Some(this) = weak.upgrade() {
273                this.popup_closed();
274            }
275        });
276
277        *this.popup_subscriptions.borrow_mut() = Some((opened, closed));
278        *this.popup.borrow_mut() = Some(popup);
279
280        let input_text_box = e.name_scope().find_as::<TextBox>("PART_EditableTextBox");
281        *this.input_text_box.borrow_mut() = input_text_box;
282    }
283}
284
285impl ItemsControlImpl for ComboBox {
286    fn create_container_for_item_override(
287        _this: &Self,
288        _item: &Option<BoxedValue>,
289        _index: i32,
290        _recycle_key: Option<RecycleKey>,
291    ) -> Ref<Control> {
292        ComboBoxItem::new().upcast()
293    }
294
295    fn needs_container_override(this: &Self, item: &Option<BoxedValue>, _index: i32) -> (bool, Option<RecycleKey>) {
296        this.needs_container::<ComboBoxItem>(item)
297    }
298}
299
300impl SelectingItemsControlImpl for ComboBox {
301    fn update_selection_from_event(this: &Self, container: &Ref<Control>, event_args: &dyn IRoutedEventArgs) -> bool {
302        if Self::parent_update_selection_from_event(this, container, event_args) {
303            let popup = this.popup.borrow().clone();
304            if let Some(popup) = popup {
305                popup.close();
306            }
307            return true;
308        }
309
310        false
311    }
312
313    fn should_trigger_selection(_this: &Self, selectable: &Visual, event_args: &PointerEventArgs) -> bool {
314        ItemSelectionEventTriggers::is_pointer_event_within_bounds(selectable, event_args)
315            && matches!(
316                event_args.properties().pointer_update_kind,
317                PointerUpdateKind::LeftButtonReleased | PointerUpdateKind::RightButtonReleased
318            )
319            && event_args.routed_event().is_some_and(|event| event == *InputElement::pointer_released_event())
320    }
321}
322
323impl ComboBox {
324    pub(crate) const PC_DROPDOWN_OPEN: &'static str = ":dropdownopen";
325    pub(crate) const PC_PRESSED: &'static str = ":pressed";
326
327    /// The named parts expected in the control template.
328    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
329        TemplatePartAttribute::required("PART_Popup", <Popup as StaticType>::TYPE),
330        TemplatePartAttribute::new("PART_EditableTextBox", <TextBox as StaticType>::TYPE),
331    ];
332
333    /// The pseudoclasses set by the class.
334    pub const PSEUDO_CLASSES: PseudoClassesAttribute =
335        PseudoClassesAttribute::new(&[Self::PC_DROPDOWN_OPEN, Self::PC_PRESSED]);
336}
337
338ferroui_base::ferro_properties! { impl ComboBox {
339    ferro_property!(
340        /// Defines the `IsDropDownOpen` property.
341        pub fn is_drop_down_open_property() -> StyledProperty<bool> {
342            FerroProperty::register::<ComboBox, _>("IsDropDownOpen", false)
343        }
344    );
345
346    ferro_property!(
347        /// Defines the `IsEditable` property.
348        pub fn is_editable_property() -> StyledProperty<bool> {
349            FerroProperty::register::<ComboBox, _>("IsEditable", false)
350        }
351    );
352
353    ferro_property!(
354        /// Defines the `MaxDropDownHeight` property.
355        pub fn max_drop_down_height_property() -> StyledProperty<f64> {
356            FerroProperty::register::<ComboBox, _>("MaxDropDownHeight", 200.0)
357        }
358    );
359
360    ferro_property!(
361        /// Defines the `SelectionBoxItem` property.
362        pub fn selection_box_item_property() -> DirectProperty<ComboBox, Option<BoxedValue>> {
363            FerroProperty::register_direct::<ComboBox, _>("SelectionBoxItem", |o| o.selection_box_item(), None, None)
364        }
365    );
366
367    ferro_property!(
368        /// Defines the `PlaceholderText` property.
369        pub fn placeholder_text_property() -> StyledProperty<Option<String>> {
370            TextBox::placeholder_text_property().add_owner::<ComboBox>()
371        }
372    );
373
374    ferro_property!(
375        /// Defines the `PlaceholderForeground` property.
376        pub fn placeholder_foreground_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
377            TextBox::placeholder_foreground_property().add_owner::<ComboBox>()
378        }
379    );
380
381    ferro_property!(
382        /// Defines the `HorizontalContentAlignment` property.
383        pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
384            ContentControl::horizontal_content_alignment_property().add_owner::<ComboBox>()
385        }
386    );
387
388    ferro_property!(
389        /// Defines the `VerticalContentAlignment` property.
390        pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
391            ContentControl::vertical_content_alignment_property().add_owner::<ComboBox>()
392        }
393    );
394
395    ferro_property!(
396        /// Defines the `Text` property.
397        pub fn text_property() -> StyledProperty<Option<String>> {
398            TextBlock::text_property().add_owner_with::<ComboBox>(
399                StyledPropertyMetadata::new(Some(Some(String::new())))
400                    .with_default_binding_mode(BindingMode::TwoWay)
401                    .with_enable_data_validation(true),
402            )
403        }
404    );
405
406    ferro_property!(
407        /// Defines the `SelectionBoxItemTemplate` property.
408        pub fn selection_box_item_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
409            FerroProperty::register_with::<ComboBox, _>(
410                "SelectionBoxItemTemplate",
411                StyledPropertyOptions::new(None)
412                    .default_binding_mode(BindingMode::TwoWay)
413                    .coerce(Self::coerce_selection_box_item_template),
414            )
415        }
416    );
417} }
418
419impl ComboBox {
420    fn coerce_selection_box_item_template(
421        obj: &FerroObject,
422        template: Option<Rc<dyn IDataTemplate>>,
423    ) -> Option<Rc<dyn IDataTemplate>> {
424        if template.is_some() {
425            return template;
426        }
427        if let Some(combo_box) = obj.downcast_ref::<ComboBox>() {
428            return combo_box.get_effective_item_template();
429        }
430        template
431    }
432
433    fn static_constructor() {
434        // The default value for the `ItemsPanel` property.
435        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
436            FuncTemplate::new(|| Some(VirtualizingStackPanel::new().upcast::<Panel>()));
437        ItemsControl::items_panel_property().override_default_value::<ComboBox>(default_panel);
438        InputElement::focusable_property().override_default_value::<ComboBox>(true);
439        SelectingItemsControl::is_text_search_enabled_property().override_default_value::<ComboBox>(true);
440        crate::platform::PlatformFeedback::feedback_type_property()
441            .override_default_value::<ComboBox>(crate::platform::FeedbackType::Auto);
442    }
443
444    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
445    pub fn construct() -> Self {
446        Self {
447            base: SelectingItemsControl::construct(),
448            popup: RefCell::new(None),
449            popup_subscriptions: RefCell::new(None),
450            selection_box_item: RefCell::new(None),
451            input_text_box: RefCell::new(None),
452            subscriptions_on_open: CompositeDisposable::new(),
453            text_value_binding_evaluator: RefCell::new(None),
454            skip_next_text_changed: Cell::new(false),
455            drop_down_closed: HandlerList::new(),
456            drop_down_opened: HandlerList::new(),
457        }
458    }
459
460    pub fn new() -> Ref<Self> {
461        instantiate(Self::construct())
462    }
463
464    /// Occurs after the drop-down (popup) list of the [`ComboBox`] closes.
465    pub fn drop_down_closed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
466        let token = self.drop_down_closed.add(Rc::new(handler));
467        let weak = self.to_ref().downgrade();
468        Disposable::create(move || {
469            if let Some(this) = weak.upgrade() {
470                this.drop_down_closed.remove(token);
471            }
472        })
473    }
474
475    /// Occurs after the drop-down (popup) list of the [`ComboBox`] opens.
476    pub fn drop_down_opened(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
477        let token = self.drop_down_opened.add(Rc::new(handler));
478        let weak = self.to_ref().downgrade();
479        Disposable::create(move || {
480            if let Some(this) = weak.upgrade() {
481                this.drop_down_opened.remove(token);
482            }
483        })
484    }
485
486    /// Gets or sets a value indicating whether the dropdown is currently
487    /// open.
488    pub fn is_drop_down_open(&self) -> bool {
489        self.get_value(Self::is_drop_down_open_property())
490    }
491
492    pub fn set_is_drop_down_open(&self, value: bool) {
493        self.set_value(Self::is_drop_down_open_property(), value)
494    }
495
496    /// Gets or sets a value indicating whether the control is editable.
497    pub fn is_editable(&self) -> bool {
498        self.get_value(Self::is_editable_property())
499    }
500
501    pub fn set_is_editable(&self, value: bool) {
502        self.set_value(Self::is_editable_property(), value)
503    }
504
505    /// Gets or sets the maximum height for the dropdown list.
506    pub fn max_drop_down_height(&self) -> f64 {
507        self.get_value(Self::max_drop_down_height_property())
508    }
509
510    pub fn set_max_drop_down_height(&self, value: f64) {
511        self.set_value(Self::max_drop_down_height_property(), value)
512    }
513
514    /// Gets the item to display as the control's content.
515    pub fn selection_box_item(&self) -> Option<BoxedValue> {
516        self.selection_box_item.borrow().clone()
517    }
518
519    /// Sets the item to display as the control's content.
520    pub fn set_selection_box_item(&self, value: Option<BoxedValue>) {
521        self.set_and_raise(Self::selection_box_item_property(), &self.selection_box_item, value);
522    }
523
524    /// Gets or sets the placeholder text.
525    pub fn placeholder_text(&self) -> Option<String> {
526        self.get_value(Self::placeholder_text_property())
527    }
528
529    pub fn set_placeholder_text(&self, value: Option<String>) {
530        self.set_value(Self::placeholder_text_property(), value)
531    }
532
533    /// Gets or sets the brush that renders the placeholder text.
534    pub fn placeholder_foreground(&self) -> Option<Rc<dyn IBrush>> {
535        self.get_value(Self::placeholder_foreground_property())
536    }
537
538    pub fn set_placeholder_foreground(&self, value: Option<Rc<dyn IBrush>>) {
539        self.set_value(Self::placeholder_foreground_property(), value)
540    }
541
542    /// Gets or sets the horizontal alignment of the content within the
543    /// control.
544    pub fn horizontal_content_alignment(&self) -> HorizontalAlignment {
545        self.get_value(Self::horizontal_content_alignment_property())
546    }
547
548    pub fn set_horizontal_content_alignment(&self, value: HorizontalAlignment) {
549        self.set_value(Self::horizontal_content_alignment_property(), value)
550    }
551
552    /// Gets or sets the vertical alignment of the content within the
553    /// control.
554    pub fn vertical_content_alignment(&self) -> VerticalAlignment {
555        self.get_value(Self::vertical_content_alignment_property())
556    }
557
558    pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
559        self.set_value(Self::vertical_content_alignment_property(), value)
560    }
561
562    /// Gets or sets the data template used to display the selected item.
563    /// This has a higher priority than the item template if set.
564    pub fn selection_box_item_template(&self) -> Option<Rc<dyn IDataTemplate>> {
565        self.get_value(Self::selection_box_item_template_property())
566    }
567
568    pub fn set_selection_box_item_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
569        self.set_value(Self::selection_box_item_template_property(), value)
570    }
571
572    /// Gets or sets the text used when the control is editable. Does
573    /// nothing if it is not editable.
574    pub fn text(&self) -> Option<String> {
575        self.get_value(Self::text_property())
576    }
577
578    pub fn set_text(&self, value: Option<String>) {
579        self.set_value(Self::text_property(), value)
580    }
581
582    pub(crate) fn item_focused(&self, drop_down_item: &ComboBoxItem) {
583        if self.is_drop_down_open() && drop_down_item.is_focused() && drop_down_item.is_arrange_valid() {
584            drop_down_item.bring_into_view();
585        }
586    }
587
588    /// Whether the popup of the template is open and contains `visual`.
589    fn is_inside_popup(&self, visual: &Visual) -> bool {
590        let popup = self.popup.borrow().clone();
591        popup.is_some_and(|popup| popup.is_inside_popup(visual))
592    }
593
594    fn popup_closed(&self) {
595        self.subscriptions_on_open.clear();
596
597        if self.is_editable() && Self::can_focus(self) {
598            self.focus();
599        }
600
601        for (_, handler) in self.drop_down_closed.snapshot().iter() {
602            handler();
603        }
604    }
605
606    fn popup_opened(&self) {
607        self.try_focus_selected_item();
608
609        self.subscriptions_on_open.clear();
610
611        let weak = self.to_ref().downgrade();
612        let is_visible_changed = Rc::new(move |is_visible: bool| {
613            if let Some(this) = weak.upgrade() {
614                this.is_visible_changed(is_visible);
615            }
616        });
617
618        let subscribe = |object: &FerroObject| {
619            let handler = is_visible_changed.clone();
620            let subscription = FerroObjectExtensions::get_observable(object, Visual::is_visible_property())
621                .subscribe_fn(move |is_visible| handler(is_visible));
622            self.subscriptions_on_open.add(subscription);
623        };
624
625        subscribe(self);
626
627        for parent in self.get_visual_ancestors().filter_map(|ancestor| ancestor.cast::<Control>()) {
628            subscribe(&parent);
629        }
630
631        self.update_flow_direction();
632
633        for (_, handler) in self.drop_down_opened.snapshot().iter() {
634            handler();
635        }
636    }
637
638    fn is_visible_changed(&self, is_visible: bool) {
639        if !is_visible && self.is_drop_down_open() {
640            self.set_current_value(Self::is_drop_down_open_property(), false);
641        }
642    }
643
644    fn try_focus_selected_item(&self) {
645        let selected_index = self.selected_index();
646        if self.is_drop_down_open() && selected_index != -1 {
647            self.scroll_into_view(selected_index);
648            let container = self.container_from_index(selected_index);
649
650            if let Some(container) = container {
651                if Self::can_focus(&container) {
652                    container.focus();
653                }
654            }
655        }
656    }
657
658    fn can_focus(control: &Control) -> bool {
659        control.focusable() && control.is_effectively_enabled() && control.is_visible()
660    }
661
662    fn update_selection_box_item(&self, item: Option<BoxedValue>) {
663        let mut item = item;
664
665        let content_control =
666            item.as_ref().and_then(Control::from_boxed).and_then(|control| control.cast::<ContentControl>());
667        if let Some(content_control) = content_control {
668            item = content_control.content();
669        }
670
671        let Some(item) = item else {
672            self.set_selection_box_item(None);
673            return;
674        };
675
676        if self.selection_box_item_template().is_some() {
677            self.set_selection_box_item(Some(item));
678        } else if let Some(control) = Control::from_boxed(&item) {
679            if self.visual_root().is_some() {
680                control.measure(Size::INFINITY);
681
682                let fill = VisualBrush::new();
683                fill.set_visual(&control);
684                fill.set_stretch(Stretch::None);
685                fill.set_alignment_x(AlignmentX::Left);
686
687                let rectangle = Rectangle::new();
688                rectangle.set_width(control.desired_size().width);
689                rectangle.set_height(control.desired_size().height);
690                rectangle.set_fill(Some(fill.into()));
691
692                self.set_selection_box_item(Some(Control::boxed(rectangle)));
693            }
694
695            self.update_flow_direction();
696        } else {
697            self.set_selection_box_item(Some(item));
698        }
699    }
700
701    fn update_flow_direction(&self) {
702        let rectangle = self
703            .selection_box_item()
704            .as_ref()
705            .and_then(Control::from_boxed)
706            .and_then(|control| control.cast::<Rectangle>());
707        if let Some(rectangle) = rectangle {
708            let content = rectangle
709                .fill()
710                .and_then(|fill| fill.as_object().and_then(|object| object.downcast_ref::<VisualBrush>()?.visual()));
711            if let Some(content) = content {
712                let flow_direction =
713                    content.visual_parent().map_or(FlowDirection::LeftToRight, |parent| parent.flow_direction());
714                rectangle.set_flow_direction(flow_direction);
715            }
716        }
717    }
718
719    fn update_input_text_from_selection(&self, item: &Option<BoxedValue>) {
720        // If we are modifying the text box which has deselected a value we
721        // don't want to update the text box value.
722        if self.skip_next_text_changed.get() {
723            return;
724        }
725        self.set_current_value(Self::text_property(), Some(self.get_item_text_value(item)));
726    }
727
728    fn select_next(&self) -> bool {
729        self.move_selection_by(self.selected_index(), 1, self.wrap_selection())
730    }
731
732    fn select_previous(&self) -> bool {
733        self.move_selection_by(self.selected_index(), -1, self.wrap_selection())
734    }
735
736    fn move_selection_by(&self, start_index: i32, step: i32, wrap: bool) -> bool {
737        fn is_selectable(o: Option<&FerroObject>) -> bool {
738            o.is_none_or(|o| o.get_value(InputElement::is_enabled_property()))
739        }
740
741        let count = self.item_count();
742
743        let mut i = start_index + step;
744        while i != start_index {
745            if i < 0 || i >= count {
746                if wrap {
747                    if i < 0 {
748                        i += count;
749                    } else if i >= count {
750                        i %= count;
751                    }
752                } else {
753                    return false;
754                }
755            }
756
757            let item = self.items_view().get_at(i as usize);
758            let container = self.container_from_index(i);
759
760            let item_object = item.as_ref().and_then(|value| ValueTypes::as_object(&**value));
761            if is_selectable(item_object.as_deref())
762                && is_selectable(container.as_deref().map(|control| -> &FerroObject { control }))
763            {
764                self.set_selected_index(i);
765                return true;
766            }
767
768            i += step;
769        }
770
771        false
772    }
773
774    /// Clears the selection.
775    pub fn clear(&self) {
776        self.set_selected_item(None);
777        self.set_selected_index(-1);
778    }
779
780    fn handle_text_value_binding_value_changed(
781        &self,
782        text_search_prop_change: Option<&FerroPropertyChangedEventArgs<'_>>,
783        display_member_prop_change: Option<&FerroPropertyChangedEventArgs<'_>>,
784    ) {
785        let new_binding = |change: Option<&FerroPropertyChangedEventArgs<'_>>| {
786            change.and_then(|change| change.get_new_value::<Option<AssignedBinding>>())
787        };
788
789        // Prioritise using the text binding of the text search if possible.
790        let text_search_binding =
791            if text_search_prop_change.is_none() { TextSearch::get_text_binding(self) } else { None };
792        let text_value_binding = text_search_binding
793            .or_else(|| new_binding(text_search_prop_change))
794            .or_else(|| new_binding(display_member_prop_change));
795
796        let evaluator = self.text_value_binding_evaluator.borrow().clone();
797        let evaluator = match (evaluator, &text_value_binding) {
798            (None, binding) => BindingEvaluator::try_create(binding.as_ref()),
799            (Some(_), None) => None,
800            (Some(evaluator), Some(binding)) => {
801                evaluator.update_binding(binding);
802                Some(evaluator)
803            }
804        };
805        *self.text_value_binding_evaluator.borrow_mut() = evaluator.clone();
806
807        // If the binding is set we want to set the initial value for the
808        // selected item so the text box has the correct value.
809        if let Some(evaluator) = evaluator {
810            let text: BoxedValue = Rc::new(self.get_item_text_value(&self.selected_value()));
811            evaluator.set_value(BindingEvaluator::value_property(), Some(text));
812        }
813    }
814
815    fn text_changed(&self, new_value: Option<String>) {
816        if !self.is_editable() || self.skip_next_text_changed.get() {
817            return;
818        }
819
820        let mut selected_idx = -1;
821        let mut selected_item = None;
822        let count = self.items().count();
823        for i in 0..count {
824            let item = self.items().get_at(i);
825            let item_text = self.get_item_text_value(&item);
826            if new_value.as_deref().is_some_and(|new_value| equals_ignore_case(new_value, &item_text)) {
827                selected_idx = i as i32;
828                selected_item = item;
829                break;
830            }
831        }
832
833        {
834            self.skip_next_text_changed.set(true);
835            let _reset = Reset(&self.skip_next_text_changed);
836            self.set_selected_index(selected_idx);
837            self.set_selected_item(selected_item);
838        }
839
840        // When changing the selected index it will set the tab once active
841        // element of this control to a combo box item; this will then break
842        // tab navigation back into this combo box as it will try to focus
843        // the combo box item rather than the combo box or editable text
844        // box, so we need to set the tab once active element to null.
845        KeyboardNavigation::set_tab_once_active_element(self, None);
846    }
847
848    fn get_item_text_value(&self, item: &Option<BoxedValue>) -> String {
849        let evaluator = self.text_value_binding_evaluator.borrow().clone();
850        TextSearch::get_effective_text(item, evaluator.as_ref())
851    }
852}
853
854/// Whether two texts are equal, comparing without regard to case.
855fn equals_ignore_case(a: &str, b: &str) -> bool {
856    a.chars().flat_map(char::to_uppercase).eq(b.chars().flat_map(char::to_uppercase))
857}