//! A combo box: a field you can type in that also opens a list. //! //! It is a [`TextInput`] that hosts a popup — an adapter, not a second text //! editor. Everything about editing (selection, the clipboard, IME, the caret, //! scrolling it into view) is the text input's, because a combo box that //! reimplemented any of that would be a second implementation of the hardest //! widget in the framework. //! //! **It holds no candidates.** The rows are ordinary deferred content, so the //! list is a `for` over application state: //! //! ```lisp //! (combo-box :text draft :on-change (filter payload) //! (for candidate matches :key candidate //! (menu-item :label candidate :on-select (choose candidate)))) //! ``` //! //! Filtering is the application's, and that is a finding rather than an //! omission: the plan expected a *completion query wire* here, on the shape of //! `:get-image`. A query exists for a value the widget discovers itself and //! the application never sees — a markdown image's `src`. A combo box's text //! is already delivered by `:on-change`, so the application can compute //! matches into state and let `for` do the rest. Pulling what has just been //! pushed would be a second path to the same value. use guiduck_scene::Fragment; use guiduck_scene::geom::{Point, Rect, Size}; use super::menu::Popup; use super::{CursorShape, TextInput, Widget}; use crate::content::ContentBuilder; use crate::dirty::Dirt; use crate::event::{EventData, EventKind, Key, KeyInput, PointerEvent}; use crate::style::ComputedStyle; use crate::text::TextContext; #[derive(Default)] pub struct ComboBox { field: TextInput, content: ContentBuilder, /// Set while the popup is up, so the same key that opened it closes it. open: bool, extra: Vec, } impl ComboBox { pub fn new(font_size: f32) -> Self { Self { field: TextInput::new(font_size), ..Default::default() } } pub fn set_text(&mut self, text: impl Into) { self.field.set_content(text); } pub fn text(&self) -> String { self.field.content() } pub fn set_font_size(&mut self, size: f32) { self.field.set_font_size(size); } pub fn set_family(&mut self, family: impl Into) { self.field.set_family(family); } /// The tree's half of opening: it tells the field whether its list is up, /// so the arrow key toggles rather than re-opening. pub fn set_menu_open(&mut self, open: bool) { self.open = open; } fn ask_to_open(&mut self) { self.extra.push(EventData::MenuOpen); } } impl Widget for ComboBox { fn popup(&self) -> Option { Some(Popup { content: self.content.clone(), beside: false, // The list belongs to the field, so it is the field's width. match_width: true, }) } fn defers_content(&self) -> bool { true } fn set_content(&mut self, content: ContentBuilder) { self.content = content; } fn measure( &mut self, text: &mut TextContext, known: taffy::Size>, available: taffy::Size, ) -> taffy::Size { self.field.measure(text, known, available) } fn finalize_layout(&mut self, text: &mut TextContext, size: Size, content: Size) { self.field.finalize_layout(text, size, content); } fn paint(&mut self, fragment: &mut Fragment, size: Size) { self.field.paint(fragment, size); } fn focusable(&self) -> bool { true } fn cursor(&self, local: Point) -> CursorShape { self.field.cursor(local) } fn on_focus_changed(&mut self, focused: bool) { self.field.on_focus_changed(focused); } fn take_wake(&mut self) -> Option { self.field.take_wake() } fn on_timer(&mut self) { self.field.on_timer(); } fn on_key( &mut self, key: &KeyInput, text: &mut TextContext, clipboard: &mut dyn crate::clipboard::Clipboard, ) -> bool { // Down opens the list — the one key a combo box takes that a plain // field does not. It is checked *before* delegating so the field // cannot consume it as caret movement, and only when the list is // closed, so the open list's own navigation still gets it. if key.key == Key::Down && !self.open { self.ask_to_open(); return true; } self.field.on_key(key, text, clipboard) } fn on_ime(&mut self, ime: &crate::event::ImeInput, text: &mut TextContext) { self.field.on_ime(ime, text); } fn on_pointer( &mut self, kind: EventKind, event: &PointerEvent, text: &mut TextContext, clipboard: &mut dyn crate::clipboard::Clipboard, ) -> bool { self.field.on_pointer(kind, event, text, clipboard) } fn ime_cursor_area(&self) -> Option { self.field.ime_cursor_area() } fn reveal_rect(&self) -> Option { self.field.reveal_rect() } fn role(&self) -> accesskit::Role { accesskit::Role::ComboBox } fn accessibility(&self, node: &mut accesskit::Node) { self.field.accessibility(node); node.set_expanded(self.open); } fn accessibility_extended( &mut self, node: &mut accesskit::Node, update: &mut accesskit::TreeUpdate, next_id: &mut dyn FnMut() -> accesskit::NodeId, origin: Point, text: &mut TextContext, ) { self.field .accessibility_extended(node, update, next_id, origin, text); node.set_expanded(self.open); } fn take_dirt(&mut self) -> Dirt { self.field.take_dirt() } fn take_emitted(&mut self) -> Vec { let mut emitted = std::mem::take(&mut self.extra); emitted.extend(self.field.take_emitted()); emitted } fn type_name(&self) -> &'static str { "combo-box" } fn apply_style(&mut self, style: &ComputedStyle) { self.field.apply_style(style); } } #[cfg(test)] mod tests;