From dd5ef8b90895f626d4b8f0466c4457c5abf451a0 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:51:29 +0100 Subject: Add ComboBox widget - Widget implementation - Widget helper - Example --- widget/src/combo_box.rs | 716 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 716 insertions(+) create mode 100644 widget/src/combo_box.rs (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs new file mode 100644 index 00000000..262f83a8 --- /dev/null +++ b/widget/src/combo_box.rs @@ -0,0 +1,716 @@ +//! Display a dropdown list of searchable and selectable options. +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::text; +use crate::core::widget::{self, Widget}; +use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell}; +use crate::overlay::menu; +use crate::text::LineHeight; +use crate::{container, scrollable, text_input, TextInput}; + +use std::cell::RefCell; +use std::fmt::Display; +use std::time::Instant; + +/// A widget for searching and selecting a single value from a list of options. +/// +/// This widget is composed by a [`TextInput`] that can be filled with the text +/// to search for corresponding values from the list of options that are displayed +/// as a [`Menu`]. +#[allow(missing_debug_implementations)] +pub struct ComboBox<'a, T, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: text_input::StyleSheet + menu::StyleSheet, +{ + state: &'a State, + text_input: TextInput<'a, TextInputEvent, Renderer>, + font: Option, + selection: text_input::Value, + on_selected: Box Message>, + on_selection: Option Message>>, + on_blur: Option, + on_input: Option Message>>, + menu_style: ::Style, + padding: Padding, + size: Option, +} + +impl<'a, T, Message, Renderer> ComboBox<'a, T, Message, Renderer> +where + T: std::fmt::Display + Clone, + Renderer: text::Renderer, + Renderer::Theme: text_input::StyleSheet + menu::StyleSheet, +{ + /// Creates a new [`ComboBox`] with the given list of options, a placeholder, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + state: &'a State, + placeholder: &str, + selection: Option<&T>, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + let text_input = TextInput::new(placeholder, &state.value()) + .on_input(TextInputEvent::TextChanged); + + let selection = selection.map(T::to_string).unwrap_or_else(String::new); + + Self { + state, + text_input, + font: None, + selection: text_input::Value::new(&selection), + on_selected: Box::new(on_selected), + on_selection: None, + on_input: None, + on_blur: None, + menu_style: Default::default(), + padding: Padding::new(0.0), + size: None, + } + } + + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`] of the [`ComboBox`]. + pub fn on_input( + mut self, + on_input: impl Fn(String) -> Message + 'static, + ) -> Self { + self.on_input = Some(Box::new(on_input)); + self + } + + /// Sets the message that will be produced when an option of the + /// [`ComboBox`] is hovered using the arrow keys. + pub fn on_selection( + mut self, + on_selection: impl Fn(T) -> Message + 'static, + ) -> Self { + self.on_selection = Some(Box::new(on_selection)); + self + } + + /// Sets the message that will be produced when the outside area + /// of the [`ComboBox`] is pressed. + pub fn on_blur(mut self, message: Message) -> Self { + self.on_blur = Some(message); + self + } + + /// Sets the [`Padding`] of the [`ComboBox`]. + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self.text_input = self.text_input.padding(self.padding); + self + } + + /// Sets the style of the [`ComboBox`]. + // TODO: Define its own `StyleSheet` trait + pub fn style(mut self, style: S) -> Self + where + S: Into<::Style> + + Into<::Style> + + Clone, + { + self.menu_style = style.clone().into(); + self.text_input = self.text_input.style(style); + self + } + + /// Sets the style of the [`TextInput`] of the [`ComboBox`]. + pub fn text_input_style(mut self, style: S) -> Self + where + S: Into<::Style> + Clone, + { + self.text_input = self.text_input.style(style); + self + } + + /// Sets the [`Font`] of the [`ComboBox`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.text_input = self.text_input.font(font); + self.font = Some(font); + self + } + + /// Sets the [`Icon`] of the [`ComboBox`]. + pub fn icon(mut self, icon: text_input::Icon) -> Self { + self.text_input = self.text_input.icon(icon); + self + } + + /// Returns whether the [`ComboBox`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.state.is_focused() + } + + /// Sets the text sixe of the [`ComboBox`]. + pub fn size(mut self, size: f32) -> Self { + self.text_input = self.text_input.size(size); + self.size = Some(size); + self + } + + /// Sets the [`LineHeight`] of the [`ComboBox`]. + pub fn line_height(self, line_height: impl Into) -> Self { + Self { + text_input: self.text_input.line_height(line_height), + ..self + } + } + + /// Sets the width of the [`ComboBox`]. + pub fn width(self, width: impl Into) -> Self { + Self { + text_input: self.text_input.width(width), + ..self + } + } +} + +/// The local state of a [`ComboBox`]. +#[derive(Debug, Clone)] +pub struct State(RefCell>); + +#[derive(Debug, Clone)] +struct Inner { + text_input: text_input::State, + value: String, + options: Vec, + option_matchers: Vec, + filtered_options: Filtered, +} + +#[derive(Debug, Clone)] +struct Filtered { + options: Vec, + updated: Instant, +} + +impl State +where + T: Display + Clone, +{ + /// Creates a new [`State`] for a [`ComboBox`] with the given list of options. + pub fn new(options: Vec) -> Self { + Self::with_selection(options, None) + } + + /// Creates a new [`State`] for a [`ComboBox`] with the given list of options + /// and selected value. + pub fn with_selection(options: Vec, selection: Option<&T>) -> Self { + let value = selection.map(T::to_string).unwrap_or_else(String::new); + + // Pre-build "matcher" strings ahead of time so that search is fast + let option_matchers = build_matchers(&options); + + let filtered_options = Filtered::new( + search(&options, &option_matchers, &value) + .cloned() + .collect(), + ); + + Self(RefCell::new(Inner { + text_input: text_input::State::new(), + value, + options, + option_matchers, + filtered_options, + })) + } + + /// Focuses the [`ComboBox`]. + pub fn focused(self) -> Self { + self.focus(); + self + } + + /// Focuses the [`ComboBox`]. + pub fn focus(&self) { + let mut inner = self.0.borrow_mut(); + + inner.text_input.focus(); + } + + /// Unfocuses the [`ComboBox`]. + pub fn unfocus(&self) { + let mut inner = self.0.borrow_mut(); + + inner.text_input.unfocus(); + } + + /// Returns whether the [`ComboBox`] is currently focused or not. + pub fn is_focused(&self) -> bool { + let inner = self.0.borrow(); + + inner.text_input.is_focused() + } + + fn value(&self) -> String { + let inner = self.0.borrow(); + + inner.value.clone() + } + + fn text_input_tree(&self) -> widget::Tree { + let inner = self.0.borrow(); + + inner.text_input_tree() + } + + fn update_text_input(&self, tree: widget::Tree) { + let mut inner = self.0.borrow_mut(); + + inner.update_text_input(tree) + } + + fn with_inner(&self, f: impl FnOnce(&Inner) -> O) -> O { + let inner = self.0.borrow(); + + f(&inner) + } + + fn with_inner_mut(&self, f: impl FnOnce(&mut Inner)) { + let mut inner = self.0.borrow_mut(); + + f(&mut inner); + } + + fn sync_filtered_options(&self, options: &mut Filtered) { + let inner = self.0.borrow(); + + inner.filtered_options.sync(options); + } +} + +impl Inner { + fn text_input_tree(&self) -> widget::Tree { + widget::Tree { + tag: widget::tree::Tag::of::(), + state: widget::tree::State::new(self.text_input.clone()), + children: vec![], + } + } + + fn update_text_input(&mut self, tree: widget::Tree) { + self.text_input = + tree.state.downcast_ref::().clone(); + } +} + +impl Filtered +where + T: Clone, +{ + fn new(options: Vec) -> Self { + Self { + options, + updated: Instant::now(), + } + } + + fn empty() -> Self { + Self { + options: vec![], + updated: Instant::now(), + } + } + + fn update(&mut self, options: Vec) { + self.options = options; + self.updated = Instant::now(); + } + + fn sync(&self, other: &mut Filtered) { + if other.updated != self.updated { + *other = self.clone(); + } + } +} + +struct Menu { + menu: menu::State, + hovered_option: Option, + new_selection: Option, + filtered_options: Filtered, +} + +#[derive(Debug, Clone)] +enum TextInputEvent { + TextChanged(String), +} + +impl<'a, T, Message, Renderer> Widget + for ComboBox<'a, T, Message, Renderer> +where + T: Display + Clone + 'static, + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + + text_input::StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet, +{ + fn width(&self) -> Length { + Widget::::width(&self.text_input) + } + + fn height(&self) -> Length { + Widget::::height(&self.text_input) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.text_input.layout(renderer, limits) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::>() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(Menu:: { + menu: menu::State::new(), + filtered_options: Filtered::empty(), + hovered_option: Some(0), + new_selection: None, + }) + } + + fn on_event( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let menu = tree.state.downcast_mut::>(); + + let started_focused = self.state.is_focused(); + // This is intended to check whether or not the message buffer was empty, + // since `Shell` does not expose such functionality. + let mut published_message_to_shell = false; + + // Create a new list of local messages + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + // Provide it to the widget + let mut tree = self.state.text_input_tree(); + let mut event_status = self.text_input.on_event( + &mut tree, + event.clone(), + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ); + self.state.update_text_input(tree); + + // Then finally react to them here + for message in local_messages { + let TextInputEvent::TextChanged(new_value) = message; + if let Some(on_input) = &self.on_input { + shell.publish((on_input)(new_value.clone())); + published_message_to_shell = true; + } + + // Couple the filtered options with the `ComboBox` + // value and only recompute them when the value changes, + // instead of doing it in every `view` call + self.state.with_inner_mut(|state| { + menu.hovered_option = Some(0); + state.value = new_value; + + state.filtered_options.update( + search( + &state.options, + &state.option_matchers, + &state.value, + ) + .cloned() + .collect(), + ); + }); + shell.invalidate_layout(); + } + + if self.state.is_focused() { + self.state.with_inner(|state| { + if let Event::Keyboard(keyboard::Event::KeyPressed { + key_code, + .. + }) = event + { + match key_code { + keyboard::KeyCode::Enter => { + if let Some(index) = &menu.hovered_option { + if let Some(option) = + state.filtered_options.options.get(*index) + { + menu.new_selection = Some(option.clone()); + } + } + + event_status = event::Status::Captured; + } + keyboard::KeyCode::Up => { + if let Some(index) = &mut menu.hovered_option { + *index = index.saturating_sub(1); + } else { + menu.hovered_option = Some(0); + } + + if let Some(on_selection) = &mut self.on_selection { + if let Some(option) = + menu.hovered_option.and_then(|index| { + state + .filtered_options + .options + .get(index) + }) + { + // Notify the selection + shell.publish((on_selection)( + option.clone(), + )); + published_message_to_shell = true; + } + } + + event_status = event::Status::Captured; + } + keyboard::KeyCode::Down => { + if let Some(index) = &mut menu.hovered_option { + *index = index.saturating_add(1).min( + state + .filtered_options + .options + .len() + .saturating_sub(1), + ); + } else { + menu.hovered_option = Some(0); + } + + if let Some(on_selection) = &mut self.on_selection { + if let Some(option) = + menu.hovered_option.and_then(|index| { + state + .filtered_options + .options + .get(index) + }) + { + // Notify the selection + shell.publish((on_selection)( + option.clone(), + )); + published_message_to_shell = true; + } + } + + event_status = event::Status::Captured; + } + _ => {} + } + } + }); + } + + // If the overlay menu has selected something + self.state.with_inner_mut(|state| { + if let Some(selection) = menu.new_selection.take() { + // Clear the value and reset the options and menu + state.value = String::new(); + state.filtered_options.update(state.options.clone()); + menu.menu = menu::State::default(); + + // Notify the selection + shell.publish((self.on_selected)(selection)); + published_message_to_shell = true; + + // Unfocus the input + let mut tree = state.text_input_tree(); + let _ = self.text_input.on_event( + &mut tree, + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )), + layout, + mouse::Cursor::Unavailable, + renderer, + clipboard, + &mut Shell::new(&mut vec![]), + ); + state.update_text_input(tree); + } + }); + + if started_focused + && !self.state.is_focused() + && !published_message_to_shell + { + if let Some(message) = self.on_blur.take() { + shell.publish(message); + } + } + + // Focus changed, invalidate widget tree to force a fresh `view` + if started_focused != self.state.is_focused() { + shell.invalidate_widgets(); + } + + event_status + } + + fn mouse_interaction( + &self, + _tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let tree = self.state.text_input_tree(); + self.text_input + .mouse_interaction(&tree, layout, cursor, viewport, renderer) + } + + fn draw( + &self, + _tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let selection = if self.state.is_focused() || self.selection.is_empty() + { + None + } else { + Some(&self.selection) + }; + + let tree = self.state.text_input_tree(); + self.text_input + .draw(&tree, renderer, theme, layout, cursor, selection); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + let Menu { + menu, + filtered_options, + hovered_option, + .. + } = tree.state.downcast_mut::>(); + + if self.state.is_focused() { + let bounds = layout.bounds(); + + self.state.sync_filtered_options(filtered_options); + + let mut menu = menu::Menu::new( + menu, + &filtered_options.options, + hovered_option, + |x| (self.on_selected)(x), + ) + .width(bounds.width) + .padding(self.padding) + .style(self.menu_style.clone()); + + if let Some(font) = self.font { + menu = menu.font(font); + } + + if let Some(size) = self.size { + menu = menu.text_size(size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } + } +} + +impl<'a, T, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + T: Display + Clone + 'static, + Message: 'a + Clone, + Renderer: text::Renderer + 'a, + Renderer::Theme: container::StyleSheet + + text_input::StyleSheet + + scrollable::StyleSheet + + menu::StyleSheet, +{ + fn from(combo_box: ComboBox<'a, T, Message, Renderer>) -> Self { + Self::new(combo_box) + } +} + +/// Search list of options for a given query. +pub fn search<'a, T, A>( + options: impl IntoIterator + 'a, + option_matchers: impl IntoIterator + 'a, + query: &'a str, +) -> impl Iterator + 'a +where + A: AsRef + 'a, +{ + let query: Vec = query + .to_lowercase() + .split(|c: char| !c.is_ascii_alphanumeric()) + .map(String::from) + .collect(); + + options + .into_iter() + .zip(option_matchers.into_iter()) + // Make sure each part of the query is found in the option + .filter_map(move |(option, matcher)| { + if query.iter().all(|part| matcher.as_ref().contains(part)) { + Some(option) + } else { + None + } + }) +} + +/// Build matchers from given list of options. +pub fn build_matchers<'a, T>( + options: impl IntoIterator + 'a, +) -> Vec +where + T: Display + 'a, +{ + options + .into_iter() + .map(|opt| { + let mut matcher = opt.to_string(); + matcher.retain(|c| c.is_ascii_alphanumeric()); + matcher.to_lowercase() + }) + .collect() +} -- cgit From 7fe3389cf10d5ca3752127df30dafbd6965a0fe9 Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:30:54 +0100 Subject: Swap unwrap_or_else to unwrap_or_default --- widget/src/combo_box.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 262f83a8..23839980 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -58,7 +58,7 @@ where let text_input = TextInput::new(placeholder, &state.value()) .on_input(TextInputEvent::TextChanged); - let selection = selection.map(T::to_string).unwrap_or_else(String::new); + let selection = selection.map(T::to_string).unwrap_or_default(); Self { state, @@ -204,7 +204,7 @@ where /// Creates a new [`State`] for a [`ComboBox`] with the given list of options /// and selected value. pub fn with_selection(options: Vec, selection: Option<&T>) -> Self { - let value = selection.map(T::to_string).unwrap_or_else(String::new); + let value = selection.map(T::to_string).unwrap_or_default(); // Pre-build "matcher" strings ahead of time so that search is fast let option_matchers = build_matchers(&options); -- cgit From 14fb723eecdf3ee667874882210ac826aa700919 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 26 Jul 2023 22:01:17 +0200 Subject: Add `Viewport` to `on_event` for `ComboBox` --- widget/src/combo_box.rs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 23839980..956a470a 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -394,6 +394,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, + viewport: &Rectangle, ) -> event::Status { let menu = tree.state.downcast_mut::>(); @@ -416,6 +417,7 @@ where renderer, clipboard, &mut local_shell, + viewport, ); self.state.update_text_input(tree); @@ -554,6 +556,7 @@ where renderer, clipboard, &mut Shell::new(&mut vec![]), + viewport, ); state.update_text_input(tree); } -- cgit From 9eb2889d09e42b250f12be9ba9ef8a470d8eeeae Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 26 Jul 2023 22:33:33 +0200 Subject: Use default padding of `TextInput` in `ComboBox` --- widget/src/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 956a470a..91a91c32 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -70,7 +70,7 @@ where on_input: None, on_blur: None, menu_style: Default::default(), - padding: Padding::new(0.0), + padding: text_input::DEFAULT_PADDING, size: None, } } -- cgit From 28d32a8b6463b5756aa7cc497c1e26e173f70bee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 26 Jul 2023 22:34:56 +0200 Subject: Fix `on_option_hovered` support in `ComboBox` --- widget/src/combo_box.rs | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 91a91c32..14fe2528 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -32,8 +32,8 @@ where font: Option, selection: text_input::Value, on_selected: Box Message>, - on_selection: Option Message>>, - on_blur: Option, + on_option_hovered: Option Message>>, + on_close: Option, on_input: Option Message>>, menu_style: ::Style, padding: Padding, @@ -66,9 +66,9 @@ where font: None, selection: text_input::Value::new(&selection), on_selected: Box::new(on_selected), - on_selection: None, + on_option_hovered: None, on_input: None, - on_blur: None, + on_close: None, menu_style: Default::default(), padding: text_input::DEFAULT_PADDING, size: None, @@ -87,18 +87,18 @@ where /// Sets the message that will be produced when an option of the /// [`ComboBox`] is hovered using the arrow keys. - pub fn on_selection( + pub fn on_option_hovered( mut self, on_selection: impl Fn(T) -> Message + 'static, ) -> Self { - self.on_selection = Some(Box::new(on_selection)); + self.on_option_hovered = Some(Box::new(on_selection)); self } /// Sets the message that will be produced when the outside area /// of the [`ComboBox`] is pressed. - pub fn on_blur(mut self, message: Message) -> Self { - self.on_blur = Some(message); + pub fn on_close(mut self, message: Message) -> Self { + self.on_close = Some(message); self } @@ -424,6 +424,7 @@ where // Then finally react to them here for message in local_messages { let TextInputEvent::TextChanged(new_value) = message; + if let Some(on_input) = &self.on_input { shell.publish((on_input)(new_value.clone())); published_message_to_shell = true; @@ -451,6 +452,20 @@ where if self.state.is_focused() { self.state.with_inner(|state| { + if !started_focused { + if let Some(on_option_hovered) = &mut self.on_option_hovered + { + let hovered_option = menu.hovered_option.unwrap_or(0); + + if let Some(option) = + state.filtered_options.options.get(hovered_option) + { + shell.publish(on_option_hovered(option.clone())); + published_message_to_shell = true; + } + } + } + if let Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. @@ -475,7 +490,9 @@ where menu.hovered_option = Some(0); } - if let Some(on_selection) = &mut self.on_selection { + if let Some(on_selection) = + &mut self.on_option_hovered + { if let Some(option) = menu.hovered_option.and_then(|index| { state @@ -507,7 +524,9 @@ where menu.hovered_option = Some(0); } - if let Some(on_selection) = &mut self.on_selection { + if let Some(on_selection) = + &mut self.on_option_hovered + { if let Some(option) = menu.hovered_option.and_then(|index| { state @@ -566,7 +585,7 @@ where && !self.state.is_focused() && !published_message_to_shell { - if let Some(message) = self.on_blur.take() { + if let Some(message) = self.on_close.take() { shell.publish(message); } } @@ -637,6 +656,7 @@ where &filtered_options.options, hovered_option, |x| (self.on_selected)(x), + self.on_option_hovered.as_deref(), ) .width(bounds.width) .padding(self.padding) -- cgit From e29754f32d03efc4b075e9b63cc554d128bc2ccf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 26 Jul 2023 22:36:50 +0200 Subject: Rename `on_selection` to `on_option_hovered` in `combo_box` --- widget/src/combo_box.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 14fe2528..93fc92b9 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -89,9 +89,9 @@ where /// [`ComboBox`] is hovered using the arrow keys. pub fn on_option_hovered( mut self, - on_selection: impl Fn(T) -> Message + 'static, + on_option_hovered: impl Fn(T) -> Message + 'static, ) -> Self { - self.on_option_hovered = Some(Box::new(on_selection)); + self.on_option_hovered = Some(Box::new(on_option_hovered)); self } @@ -490,7 +490,7 @@ where menu.hovered_option = Some(0); } - if let Some(on_selection) = + if let Some(on_option_hovered) = &mut self.on_option_hovered { if let Some(option) = @@ -502,7 +502,7 @@ where }) { // Notify the selection - shell.publish((on_selection)( + shell.publish((on_option_hovered)( option.clone(), )); published_message_to_shell = true; @@ -524,7 +524,7 @@ where menu.hovered_option = Some(0); } - if let Some(on_selection) = + if let Some(on_option_hovered) = &mut self.on_option_hovered { if let Some(option) = @@ -536,7 +536,7 @@ where }) { // Notify the selection - shell.publish((on_selection)( + shell.publish((on_option_hovered)( option.clone(), )); published_message_to_shell = true; -- cgit From 32a95171d2c2a7a7ac8c141277b8c3555dbb3e77 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 31 Jul 2023 22:59:42 +0200 Subject: cycle combobox with keybinds --- widget/src/combo_box.rs | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 93fc92b9..c9397433 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -468,11 +468,13 @@ where if let Event::Keyboard(keyboard::Event::KeyPressed { key_code, + modifiers, .. }) = event { - match key_code { - keyboard::KeyCode::Enter => { + let shift_modifer = modifiers.shift(); + match (key_code, shift_modifer) { + (keyboard::KeyCode::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = state.filtered_options.options.get(*index) @@ -483,9 +485,19 @@ where event_status = event::Status::Captured; } - keyboard::KeyCode::Up => { + + (keyboard::KeyCode::Up, _) + | (keyboard::KeyCode::Tab, true) => { if let Some(index) = &mut menu.hovered_option { - *index = index.saturating_sub(1); + if *index == 0 { + *index = state + .filtered_options + .options + .len() + .saturating_sub(1); + } else { + *index = index.saturating_sub(1); + } } else { menu.hovered_option = Some(0); } @@ -511,15 +523,28 @@ where event_status = event::Status::Captured; } - keyboard::KeyCode::Down => { + (keyboard::KeyCode::Down, _) + | (keyboard::KeyCode::Tab, false) + if !modifiers.shift() => + { if let Some(index) = &mut menu.hovered_option { - *index = index.saturating_add(1).min( - state + if *index + == state .filtered_options .options .len() - .saturating_sub(1), - ); + .saturating_sub(1) + { + *index = 0; + } else { + *index = index.saturating_add(1).min( + state + .filtered_options + .options + .len() + .saturating_sub(1), + ); + } } else { menu.hovered_option = Some(0); } -- cgit From e1da5fa63525cf749ec5ebbef42703fb761a0dd1 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 31 Jul 2023 23:07:35 +0200 Subject: Update widget/src/combo_box.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Héctor Ramón --- widget/src/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index c9397433..5e36ec57 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -529,7 +529,7 @@ where { if let Some(index) = &mut menu.hovered_option { if *index - == state + >= state .filtered_options .options .len() -- cgit From 8ed06dc356e0296f0b800d4d6b92998bd4444cc6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 26 Aug 2023 01:34:42 +0200 Subject: Fix `clippy` lints for Rust 1.72 --- widget/src/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 5e36ec57..690ef27c 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -735,7 +735,7 @@ where options .into_iter() - .zip(option_matchers.into_iter()) + .zip(option_matchers) // Make sure each part of the query is found in the option .filter_map(move |(option, matcher)| { if query.iter().all(|part| matcher.as_ref().contains(part)) { -- cgit From ed3454301e663a7cb7d73cd56b57b188f4d14a2f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 30 Aug 2023 04:31:21 +0200 Subject: Implement explicit text caching in the widget state tree --- widget/src/combo_box.rs | 167 ++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 92 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 690ef27c..8c20ae8e 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -144,11 +144,6 @@ where self } - /// Returns whether the [`ComboBox`] is currently focused or not. - pub fn is_focused(&self) -> bool { - self.state.is_focused() - } - /// Sets the text sixe of the [`ComboBox`]. pub fn size(mut self, size: f32) -> Self { self.text_input = self.text_input.size(size); @@ -179,7 +174,6 @@ pub struct State(RefCell>); #[derive(Debug, Clone)] struct Inner { - text_input: text_input::State, value: String, options: Vec, option_matchers: Vec, @@ -216,7 +210,6 @@ where ); Self(RefCell::new(Inner { - text_input: text_input::State::new(), value, options, option_matchers, @@ -224,51 +217,12 @@ where })) } - /// Focuses the [`ComboBox`]. - pub fn focused(self) -> Self { - self.focus(); - self - } - - /// Focuses the [`ComboBox`]. - pub fn focus(&self) { - let mut inner = self.0.borrow_mut(); - - inner.text_input.focus(); - } - - /// Unfocuses the [`ComboBox`]. - pub fn unfocus(&self) { - let mut inner = self.0.borrow_mut(); - - inner.text_input.unfocus(); - } - - /// Returns whether the [`ComboBox`] is currently focused or not. - pub fn is_focused(&self) -> bool { - let inner = self.0.borrow(); - - inner.text_input.is_focused() - } - fn value(&self) -> String { let inner = self.0.borrow(); inner.value.clone() } - fn text_input_tree(&self) -> widget::Tree { - let inner = self.0.borrow(); - - inner.text_input_tree() - } - - fn update_text_input(&self, tree: widget::Tree) { - let mut inner = self.0.borrow_mut(); - - inner.update_text_input(tree) - } - fn with_inner(&self, f: impl FnOnce(&Inner) -> O) -> O { let inner = self.0.borrow(); @@ -288,21 +242,6 @@ where } } -impl Inner { - fn text_input_tree(&self) -> widget::Tree { - widget::Tree { - tag: widget::tree::Tag::of::(), - state: widget::tree::State::new(self.text_input.clone()), - children: vec![], - } - } - - fn update_text_input(&mut self, tree: widget::Tree) { - self.text_input = - tree.state.downcast_ref::().clone(); - } -} - impl Filtered where T: Clone, @@ -366,10 +305,11 @@ where fn layout( &self, + tree: &widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.text_input.layout(renderer, limits) + self.text_input.layout(tree, renderer, limits) } fn tag(&self) -> widget::tree::Tag { @@ -385,6 +325,10 @@ where }) } + fn children(&self) -> Vec { + vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _>)] + } + fn on_event( &mut self, tree: &mut widget::Tree, @@ -398,7 +342,13 @@ where ) -> event::Status { let menu = tree.state.downcast_mut::>(); - let started_focused = self.state.is_focused(); + let started_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; // This is intended to check whether or not the message buffer was empty, // since `Shell` does not expose such functionality. let mut published_message_to_shell = false; @@ -408,9 +358,8 @@ where let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget - let mut tree = self.state.text_input_tree(); let mut event_status = self.text_input.on_event( - &mut tree, + &mut tree.children[0], event.clone(), layout, cursor, @@ -419,7 +368,6 @@ where &mut local_shell, viewport, ); - self.state.update_text_input(tree); // Then finally react to them here for message in local_messages { @@ -450,7 +398,15 @@ where shell.invalidate_layout(); } - if self.state.is_focused() { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + if is_focused { self.state.with_inner(|state| { if !started_focused { if let Some(on_option_hovered) = &mut self.on_option_hovered @@ -589,9 +545,8 @@ where published_message_to_shell = true; // Unfocus the input - let mut tree = state.text_input_tree(); let _ = self.text_input.on_event( - &mut tree, + &mut tree.children[0], Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, )), @@ -602,21 +557,25 @@ where &mut Shell::new(&mut vec![]), viewport, ); - state.update_text_input(tree); } }); - if started_focused - && !self.state.is_focused() - && !published_message_to_shell - { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + if started_focused && !is_focused && !published_message_to_shell { if let Some(message) = self.on_close.take() { shell.publish(message); } } // Focus changed, invalidate widget tree to force a fresh `view` - if started_focused != self.state.is_focused() { + if started_focused != is_focused { shell.invalidate_widgets(); } @@ -625,20 +584,24 @@ where fn mouse_interaction( &self, - _tree: &widget::Tree, + tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let tree = self.state.text_input_tree(); - self.text_input - .mouse_interaction(&tree, layout, cursor, viewport, renderer) + self.text_input.mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn draw( &self, - _tree: &widget::Tree, + tree: &widget::Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, @@ -646,16 +609,28 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let selection = if self.state.is_focused() || self.selection.is_empty() - { + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + let selection = if is_focused || self.selection.is_empty() { None } else { Some(&self.selection) }; - let tree = self.state.text_input_tree(); - self.text_input - .draw(&tree, renderer, theme, layout, cursor, selection); + self.text_input.draw( + &tree.children[0], + renderer, + theme, + layout, + cursor, + selection, + ); } fn overlay<'b>( @@ -664,14 +639,22 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { - let Menu { - menu, - filtered_options, - hovered_option, - .. - } = tree.state.downcast_mut::>(); + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + if is_focused { + let Menu { + menu, + filtered_options, + hovered_option, + .. + } = tree.state.downcast_mut::>(); - if self.state.is_focused() { let bounds = layout.bounds(); self.state.sync_filtered_options(filtered_options); -- cgit From a026e917d3364e58fd827995261158d8cb356ce9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 30 Aug 2023 06:36:24 +0200 Subject: Make `widget::Tree` mutable in `Widget::layout` --- widget/src/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 8c20ae8e..650954ef 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -305,7 +305,7 @@ where fn layout( &self, - tree: &widget::Tree, + tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { -- cgit From 89d9f1d7d2202029028a487df1dd11b0665a7517 Mon Sep 17 00:00:00 2001 From: Matthias Vogelgesang Date: Sat, 9 Sep 2023 12:24:47 +0200 Subject: Fix majority of unresolved documentation links --- widget/src/combo_box.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 690ef27c..0dc12354 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -20,7 +20,7 @@ use std::time::Instant; /// /// This widget is composed by a [`TextInput`] that can be filled with the text /// to search for corresponding values from the list of options that are displayed -/// as a [`Menu`]. +/// as a Menu. #[allow(missing_debug_implementations)] pub struct ComboBox<'a, T, Message, Renderer = crate::Renderer> where @@ -131,14 +131,16 @@ where self } - /// Sets the [`Font`] of the [`ComboBox`]. + /// Sets the [`Renderer::Font`] of the [`ComboBox`]. + /// + /// [`Renderer::Font`]: text::Renderer pub fn font(mut self, font: Renderer::Font) -> Self { self.text_input = self.text_input.font(font); self.font = Some(font); self } - /// Sets the [`Icon`] of the [`ComboBox`]. + /// Sets the [`text_input::Icon`] of the [`ComboBox`]. pub fn icon(mut self, icon: text_input::Icon) -> Self { self.text_input = self.text_input.icon(icon); self -- cgit From bc1bde0d5ca1ec291f13e108f1543daa75b97848 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 10 Sep 2023 03:36:31 +0200 Subject: Fix `ComboBox` widget not displaying selection text --- widget/src/combo_box.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index d6915281..044dc0ef 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -311,7 +311,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.text_input.layout(tree, renderer, limits) + let is_focused = { + let text_input_state = tree.children[0] + .state + .downcast_ref::>(); + + text_input_state.is_focused() + }; + + self.text_input.layout( + &mut tree.children[0], + renderer, + limits, + (!is_focused).then_some(&self.selection), + ) } fn tag(&self) -> widget::tree::Tag { @@ -665,7 +678,15 @@ where menu, &filtered_options.options, hovered_option, - |x| (self.on_selected)(x), + |x| { + tree.children[0] + .state + .downcast_mut::>( + ) + .unfocus(); + + (self.on_selected)(x) + }, self.on_option_hovered.as_deref(), ) .width(bounds.width) -- cgit From 90bd581d8ed72ac1a927a90ba04ebdd156fa2922 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 10 Sep 2023 10:18:58 +0200 Subject: Fix `ComboBox` widget panic on wasm --- widget/src/combo_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/combo_box.rs') diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 044dc0ef..768c2402 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -6,6 +6,7 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::text; +use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell}; use crate::overlay::menu; @@ -14,7 +15,6 @@ use crate::{container, scrollable, text_input, TextInput}; use std::cell::RefCell; use std::fmt::Display; -use std::time::Instant; /// A widget for searching and selecting a single value from a list of options. /// -- cgit