diff options
author | 2022-02-16 17:07:25 +0700 | |
---|---|---|
committer | 2022-02-16 17:07:25 +0700 | |
commit | 019af8ddbf96680ffcee2b3407819e90575760cb (patch) | |
tree | 45be429d1407d6ebfa46ff05e6fe5eb0fb5dd7e8 | |
parent | 35e9b75e415ef3b9124051696b60628ef56afe47 (diff) | |
download | iced-019af8ddbf96680ffcee2b3407819e90575760cb.tar.gz iced-019af8ddbf96680ffcee2b3407819e90575760cb.tar.bz2 iced-019af8ddbf96680ffcee2b3407819e90575760cb.zip |
Add `overlay` support in `iced_pure` and port `PickList` :tada:
-rw-r--r-- | native/src/widget/pick_list.rs | 604 | ||||
-rw-r--r-- | pure/src/lib.rs | 13 | ||||
-rw-r--r-- | pure/src/overlay.rs | 21 | ||||
-rw-r--r-- | pure/src/widget.rs | 12 | ||||
-rw-r--r-- | pure/src/widget/button.rs | 14 | ||||
-rw-r--r-- | pure/src/widget/column.rs | 10 | ||||
-rw-r--r-- | pure/src/widget/container.rs | 14 | ||||
-rw-r--r-- | pure/src/widget/pick_list.rs | 245 | ||||
-rw-r--r-- | pure/src/widget/row.rs | 10 | ||||
-rw-r--r-- | pure/src/widget/scrollable.rs | 14 |
10 files changed, 717 insertions, 240 deletions
diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index a200fb13..978b0cbc 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -23,11 +23,7 @@ pub struct PickList<'a, T, Message, Renderer: text::Renderer> where [T]: ToOwned<Owned = Vec<T>>, { - menu: &'a mut menu::State, - keyboard_modifiers: &'a mut keyboard::Modifiers, - is_open: &'a mut bool, - hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + state: &'a mut State<T>, on_selected: Box<dyn Fn(T) -> Message>, options: Cow<'a, [T]>, placeholder: Option<String>, @@ -49,8 +45,9 @@ pub struct State<T> { last_selection: Option<T>, } -impl<T> Default for State<T> { - fn default() -> Self { +impl<T> State<T> { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { Self { menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), @@ -61,6 +58,12 @@ impl<T> Default for State<T> { } } +impl<T> Default for State<T> { + fn default() -> Self { + Self::new() + } +} + impl<'a, T: 'a, Message, Renderer: text::Renderer> PickList<'a, T, Message, Renderer> where @@ -79,20 +82,8 @@ where selected: Option<T>, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { - let State { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, - } = state; - Self { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, + state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -145,146 +136,152 @@ where } } -impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> - for PickList<'a, T, Message, Renderer> +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, - Renderer: text::Renderer + 'a, + Renderer: text::Renderer, + T: ToString, { - fn width(&self) -> Length { - self.width - } + use std::f32; - fn height(&self) -> Length { - Length::Shrink - } + let limits = limits.width(width).height(Length::Shrink).pad(padding); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits - .width(self.width) - .height(Length::Shrink) - .pad(self.padding); - - let text_size = self.text_size.unwrap_or(renderer.default_size()); - let font = self.font.clone(); - - let max_width = match self.width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let (width, _) = renderer.measure( - label, - text_size, - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }; + let text_size = text_size.unwrap_or(renderer.default_size()); - let labels = self.options.iter().map(ToString::to_string); + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font.clone(), + Size::new(f32::INFINITY, f32::INFINITY), + ); - let labels_width = - labels.map(|label| measure(&label)).max().unwrap_or(100); + width.round() as u32 + }; - let placeholder_width = self - .placeholder - .as_ref() - .map(String::as_str) - .map(measure) - .unwrap_or(100); + let labels = options.iter().map(ToString::to_string); - labels_width.max(placeholder_width) - } - _ => 0, - }; + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); - let size = { - let intrinsic = Size::new( - max_width as f32 - + f32::from(text_size) - + f32::from(self.padding.left), - f32::from(text_size), - ); + let placeholder_width = placeholder.map(measure).unwrap_or(100); - limits.resolve(intrinsic).pad(self.padding) - }; + labels_width.max(placeholder_width) + } + _ => 0, + }; - layout::Node::new(size) - } + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size) + f32::from(padding.left), + f32::from(text_size), + ); - fn hash_layout(&self, state: &mut Hasher) { - use std::hash::Hash as _; + limits.resolve(intrinsic).pad(padding) + }; - match self.width { - Length::Shrink => { - self.placeholder.hash(state); + layout::Node::new(size) +} - self.options - .iter() - .map(ToString::to_string) - .for_each(|label| label.hash(state)); - } - _ => { - self.width.hash(state); - } +/// Hashes the layout attributes of a [`PickList`]. +pub fn hash_layout<T>( + state: &mut Hasher, + width: Length, + padding: Padding, + text_size: Option<u16>, + placeholder: Option<&str>, + options: &[T], +) where + T: ToString, +{ + use std::hash::Hash as _; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + padding.hash(state); + text_size.hash(state); + + match width { + Length::Shrink => { + placeholder.hash(state); + + options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + width.hash(state); } } +} - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let event_status = if *self.is_open { - // TODO: Encode cursor availability in the type system - *self.is_open = - cursor_position.x < 0.0 || cursor_position.y < 0.0; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - let selected = self.selected.as_ref(); - - *self.is_open = true; - *self.hovered_option = self - .options - .iter() - .position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State<T>, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); - if let Some(last_selection) = self.last_selection.take() { - shell.publish((self.on_selected)(last_selection)); + let event_status = if state.is_open { + // TODO: Encode cursor availability in the type system + state.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; - *self.is_open = false; + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); - event::Status::Captured - } else { - event_status - } + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) if self.keyboard_modifiers.command() + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() && layout.bounds().contains(cursor_position) - && !*self.is_open => + && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, @@ -296,34 +293,230 @@ where } let next_option = if y < 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter()) + if let Some(selected) = selected { + find_next(selected, options.iter()) } else { - self.options.first() + options.first() } } else if y > 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter().rev()) + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) } else { - self.options.last() + options.last() } } else { None }; if let Some(next_option) = next_option { - shell.publish((self.on_selected)(next_option.clone())); + shell.publish((on_selected)(next_option.clone())); } event::Status::Captured - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - *self.keyboard_modifiers = modifiers; - + } else { event::Status::Ignored } - _ => event::Status::Ignored, } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State<T>, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + options: &'a [T], + style_sheet: &dyn StyleSheet, +) -> Option<overlay::Element<'a, Message, Renderer>> +where + Message: 'a, + Renderer: text::Renderer + 'a, + T: Clone + ToString, +{ + if state.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut state.menu, + options, + &mut state.hovered_option, + &mut state.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(padding) + .font(font) + .style(style_sheet.menu()); + + if let Some(text_size) = text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } +} + +/// Draws a [`PickList`]. +pub fn draw<T, Renderer>( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + style_sheet: &dyn StyleSheet, +) where + Renderer: text::Renderer, + T: ToString, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let is_selected = selected.is_some(); + + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + renderer.fill_text(Text { + content: &Renderer::ARROW_DOWN_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding.horizontal()), + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + + let label = selected.map(ToString::to_string); + + if let Some(label) = + label.as_ref().map(String::as_str).or_else(|| placeholder) + { + renderer.fill_text(Text { + content: label, + size: f32::from(text_size.unwrap_or(renderer.default_size())), + font: font.clone(), + color: is_selected + .then(|| style.text_color) + .unwrap_or(style.placeholder_color), + bounds: Rectangle { + x: bounds.x + f32::from(padding.left), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }) + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + hash_layout( + state, + self.width, + self.padding, + self.text_size, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || &mut self.state, + ) } fn mouse_interaction( @@ -333,14 +526,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( @@ -351,66 +537,17 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = self.selected.is_some(); - - let style = if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius, - }, - style.background, - ); - - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - - f32::from(self.padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - - if let Some(label) = self - .selected - .as_ref() - .map(ToString::to_string) - .as_ref() - .or_else(|| self.placeholder.as_ref()) - { - renderer.fill_text(Text { - content: label, - size: f32::from( - self.text_size.unwrap_or(renderer.default_size()), - ), - font: self.font.clone(), - color: is_selected - .then(|| style.text_color) - .unwrap_or(style.placeholder_color), - bounds: Rectangle { - x: bounds.x + f32::from(self.padding.left), - y: bounds.center_y(), - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }) - } + draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) } fn overlay( @@ -418,28 +555,15 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option<overlay::Element<'_, Message, Renderer>> { - if *self.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut self.menu, - &self.options, - &mut self.hovered_option, - &mut self.last_selection, - ) - .width(bounds.width.round() as u16) - .padding(self.padding) - .font(self.font.clone()) - .style(self.style_sheet.menu()); - - if let Some(text_size) = self.text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } + overlay( + layout, + &mut self.state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) } } diff --git a/pure/src/lib.rs b/pure/src/lib.rs index 07f068cc..bab3bbc7 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -1,3 +1,4 @@ +pub mod overlay; pub mod widget; pub(crate) mod flex; @@ -129,6 +130,18 @@ where renderer, ) } + + fn overlay( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.element.as_widget_mut().overlay( + &mut self.state.state_tree, + layout, + renderer, + ) + } } impl<'a, Message, Renderer> Into<iced_native::Element<'a, Message, Renderer>> diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs new file mode 100644 index 00000000..b009fde8 --- /dev/null +++ b/pure/src/overlay.rs @@ -0,0 +1,21 @@ +use crate::Tree; + +use iced_native::Layout; + +pub use iced_native::overlay::*; + +pub fn from_children<'a, Message, Renderer>( + children: &'a mut [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option<Element<'a, Message, Renderer>> { + children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget_mut().overlay(state, layout, renderer) + }) + .next() +} diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 03b668d3..6dda653d 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -5,6 +5,7 @@ mod checkbox; mod column; mod container; mod element; +mod pick_list; mod radio; mod row; mod scrollable; @@ -21,6 +22,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use image::Image; +pub use pick_list::PickList; pub use radio::Radio; pub use row::Row; pub use scrollable::Scrollable; @@ -34,6 +36,7 @@ pub use tree::Tree; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; use iced_native::mouse; +use iced_native::overlay; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; @@ -97,6 +100,15 @@ pub trait Widget<Message, Renderer> { ) -> event::Status { event::Status::Ignored } + + fn overlay<'a>( + &'a mut self, + _state: &'a mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'a, Message, Renderer>> { + None + } } pub fn container<'a, Message, Renderer>( diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 55cbf8b4..f5e78933 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -1,3 +1,4 @@ +use crate::overlay; use crate::widget::tree::{self, Tree}; use crate::widget::{Element, Widget}; @@ -206,6 +207,19 @@ where self.on_press.is_some(), ) } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 4ab3e00d..1f025335 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -1,4 +1,5 @@ use crate::flex; +use crate::overlay; use crate::widget::{Element, Tree, Widget}; use iced_native::event::{self, Event}; @@ -216,6 +217,15 @@ where child.as_widget().hash_layout(state); } } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } } impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index f42b127d..8ad6a064 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -5,6 +5,7 @@ use iced_native::alignment; use iced_native::event::{self, Event}; use iced_native::layout; use iced_native::mouse; +use iced_native::overlay; use iced_native::renderer; use iced_native::widget::container; use iced_native::{ @@ -237,6 +238,19 @@ where self.content.as_widget().hash_layout(state); } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs new file mode 100644 index 00000000..324950e1 --- /dev/null +++ b/pure/src/widget/pick_list.rs @@ -0,0 +1,245 @@ +//! Display a dropdown list of selectable values. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::pick_list; +use iced_native::{ + Clipboard, Hasher, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::borrow::Cow; + +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: text::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + on_selected: Box<dyn Fn(T) -> Message + 'a>, + options: Cow<'a, [T]>, + placeholder: Option<String>, + selected: Option<T>, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T: 'a, Message, Renderer: text::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// The default padding of a [`PickList`]. + pub const DEFAULT_PADDING: Padding = Padding::new(5); + + /// Creates a new [`PickList`] with the given [`State`], a list of options, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, + ) -> Self { + Self { + on_selected: Box::new(on_selected), + options: options.into(), + placeholder: None, + selected, + width: Length::Shrink, + text_size: None, + padding: Self::DEFAULT_PADDING, + font: Default::default(), + style_sheet: Default::default(), + } + } + + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + + /// Sets the width of the [`PickList`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<pick_list::State<T>>() + } + + fn state(&self) -> tree::State { + tree::State::new(pick_list::State::<T>::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + pick_list::layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + pick_list::hash_layout( + state, + self.width, + self.padding, + self.text_size, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + pick_list::update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<pick_list::State<T>>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + pick_list::mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + pick_list::draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_mut::<pick_list::State<T>>(); + + pick_list::overlay( + layout, + state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) + } +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 1f281446..29128589 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -1,4 +1,5 @@ use crate::flex; +use crate::overlay; use crate::widget::{Element, Tree, Widget}; use iced_native::event::{self, Event}; @@ -202,6 +203,15 @@ where child.as_widget().hash_layout(state); } } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } } impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index c3289f9e..6653125e 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -1,3 +1,4 @@ +use crate::overlay; use crate::widget::tree::{self, Tree}; use crate::{Element, Widget}; @@ -230,6 +231,19 @@ where }, ) } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> |