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
37struct Reset<'a>(&'a Cell<bool>);
39
40impl Drop for Reset<'_> {
41 fn drop(&mut self) {
42 self.0.set(false);
43 }
44}
45
46#[repr(C)]
48pub struct ComboBox {
49 base: SelectingItemsControl,
50 popup: RefCell<Option<Ref<Popup>>>,
51 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 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 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 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 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 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 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 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 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 pub fn is_drop_down_open_property() -> StyledProperty<bool> {
342 FerroProperty::register::<ComboBox, _>("IsDropDownOpen", false)
343 }
344 );
345
346 ferro_property!(
347 pub fn is_editable_property() -> StyledProperty<bool> {
349 FerroProperty::register::<ComboBox, _>("IsEditable", false)
350 }
351 );
352
353 ferro_property!(
354 pub fn max_drop_down_height_property() -> StyledProperty<f64> {
356 FerroProperty::register::<ComboBox, _>("MaxDropDownHeight", 200.0)
357 }
358 );
359
360 ferro_property!(
361 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 pub fn placeholder_text_property() -> StyledProperty<Option<String>> {
370 TextBox::placeholder_text_property().add_owner::<ComboBox>()
371 }
372 );
373
374 ferro_property!(
375 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 pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
384 ContentControl::horizontal_content_alignment_property().add_owner::<ComboBox>()
385 }
386 );
387
388 ferro_property!(
389 pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
391 ContentControl::vertical_content_alignment_property().add_owner::<ComboBox>()
392 }
393 );
394
395 ferro_property!(
396 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 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 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 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 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 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 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 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 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 pub fn selection_box_item(&self) -> Option<BoxedValue> {
516 self.selection_box_item.borrow().clone()
517 }
518
519 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 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 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 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 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 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 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 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 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 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 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 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 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
854fn 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}