From afd9274de26ccf65285df02007b4ddb697bea9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 18 Apr 2020 14:42:48 +0200 Subject: [PATCH] Draft `ComboBox` and `Menu` layer --- examples/combo_box/src/main.rs | 196 ++++++++------------ glow/src/lib.rs | 2 +- glow/src/widget.rs | 3 + glow/src/widget/combo_box.rs | 3 + graphics/src/layer.rs | 2 + graphics/src/layer/menu.rs | 102 +++++++++++ graphics/src/lib.rs | 2 +- graphics/src/widget.rs | 1 + graphics/src/widget/combo_box.rs | 67 +++++++ native/src/element.rs | 25 ++- native/src/layer.rs | 5 + native/src/layer/menu.rs | 305 +++++++++++++++++++++++++++++++ native/src/lib.rs | 4 +- native/src/overlay.rs | 100 +++++++++- native/src/user_interface.rs | 16 +- native/src/widget.rs | 3 + native/src/widget/combo_box.rs | 256 ++++++++++++++++++++++++++ native/src/widget/scrollable.rs | 6 +- src/widget.rs | 10 +- wgpu/src/lib.rs | 2 +- wgpu/src/widget.rs | 3 + wgpu/src/widget/combo_box.rs | 3 + 22 files changed, 966 insertions(+), 150 deletions(-) create mode 100644 glow/src/widget/combo_box.rs create mode 100644 graphics/src/layer/menu.rs create mode 100644 graphics/src/widget/combo_box.rs create mode 100644 native/src/layer/menu.rs create mode 100644 native/src/widget/combo_box.rs create mode 100644 wgpu/src/widget/combo_box.rs diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index 742378c0..75a3f713 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,121 +1,6 @@ -mod combo_box { - use iced_native::{ - layout, mouse, Background, Color, Element, Hasher, Layer, Layout, - Length, Overlay, Point, Size, Vector, Widget, - }; - use iced_wgpu::{Defaults, Primitive, Renderer}; - - pub struct ComboBox; - - impl ComboBox { - pub fn new() -> Self { - Self - } - } - - impl<'a, Message> Widget<'a, Message, Renderer> for ComboBox { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - layout::Node::new(Size::new(50.0, 50.0)) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, mouse::Interaction) { - let primitive = Primitive::Quad { - bounds: layout.bounds(), - background: Background::Color(Color::BLACK), - border_width: 0, - border_radius: 0, - border_color: Color::TRANSPARENT, - }; - - (primitive, mouse::Interaction::default()) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - ) -> Option> { - Some(Overlay::new(layout.position(), Box::new(Menu))) - } - } - - impl<'a, Message> Into> for ComboBox { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } - - pub struct Menu; - - impl Layer for Menu { - fn layout( - &self, - _renderer: &Renderer, - _bounds: Size, - position: Point, - ) -> layout::Node { - let mut node = layout::Node::new(Size::new(100.0, 100.0)); - - node.move_to(position + Vector::new(25.0, 25.0)); - - node - } - - fn hash_layout(&self, state: &mut Hasher, position: Point) { - use std::hash::Hash; - - (position.x as u32).hash(state); - (position.y as u32).hash(state); - } - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, mouse::Interaction) { - let primitive = Primitive::Quad { - bounds: layout.bounds(), - background: Background::Color(Color { - r: 0.0, - g: 0.0, - b: 1.0, - a: 0.5, - }), - border_width: 0, - border_radius: 0, - border_color: Color::TRANSPARENT, - }; - - (primitive, mouse::Interaction::default()) - } - } -} - -pub use combo_box::ComboBox; - use iced::{ - button, Button, Column, Container, Element, Length, Sandbox, Settings, Text, + button, combo_box, Button, Column, ComboBox, Container, Element, Length, + Sandbox, Settings, Text, }; pub fn main() { @@ -125,11 +10,14 @@ pub fn main() { #[derive(Default)] struct Example { button: button::State, + combo_box: combo_box::State, + selected_language: Language, } #[derive(Debug, Clone, Copy)] enum Message { ButtonPressed, + LanguageSelected(Language), } impl Sandbox for Example { @@ -143,15 +31,36 @@ impl Sandbox for Example { String::from("Combo box - Iced") } - fn update(&mut self, _message: Message) {} + fn update(&mut self, message: Message) { + match message { + Message::ButtonPressed => {} + Message::LanguageSelected(language) => { + self.selected_language = language; + } + } + } fn view(&mut self) -> Element { - let combo_box = ComboBox::new(); + let combo_box = ComboBox::new( + &mut self.combo_box, + &Language::ALL[..], + Some(self.selected_language), + Message::LanguageSelected, + ); let button = Button::new(&mut self.button, Text::new("Press me!")) .on_press(Message::ButtonPressed); - let content = Column::new().spacing(10).push(combo_box).push(button); + let mut content = Column::new() + .spacing(10) + .push(Text::new("Which is your favorite language?")) + .push(combo_box); + + if self.selected_language == Language::Javascript { + content = content.push(Text::new("You are wrong!")); + } + + content = content.push(button); Container::new(content) .width(Length::Fill) @@ -161,3 +70,50 @@ impl Sandbox for Example { .into() } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Javascript, + Other, +} + +impl Language { + const ALL: [Language; 7] = [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Javascript, + Language::Other, + ]; +} + +impl Default for Language { + fn default() -> Language { + Language::Rust + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Javascript => "Javascript", + Language::Other => "Some other language", + } + ) + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs index a6c8a75a..bdd854e3 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -2,7 +2,7 @@ //! //! [`glow`]: https://github.com/grovesNL/glow //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(rust_2018_idioms)] diff --git a/glow/src/widget.rs b/glow/src/widget.rs index 9968092b..c8f16725 100644 --- a/glow/src/widget.rs +++ b/glow/src/widget.rs @@ -11,6 +11,7 @@ use crate::Renderer; pub mod button; pub mod checkbox; +pub mod combo_box; pub mod container; pub mod pane_grid; pub mod progress_bar; @@ -24,6 +25,8 @@ pub use button::Button; #[doc(no_inline)] pub use checkbox::Checkbox; #[doc(no_inline)] +pub use combo_box::ComboBox; +#[doc(no_inline)] pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; diff --git a/glow/src/widget/combo_box.rs b/glow/src/widget/combo_box.rs new file mode 100644 index 00000000..bb3931ef --- /dev/null +++ b/glow/src/widget/combo_box.rs @@ -0,0 +1,3 @@ +pub use iced_native::combo_box::State; + +pub type ComboBox<'a, T, Message> = iced_native::ComboBox<'a, T, Message>; diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 6aca738e..ddf835a4 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -1,4 +1,6 @@ //! Organize rendering primitives into a flattened list of layers. +mod menu; + use crate::image; use crate::svg; use crate::triangle; diff --git a/graphics/src/layer/menu.rs b/graphics/src/layer/menu.rs new file mode 100644 index 00000000..e94ef964 --- /dev/null +++ b/graphics/src/layer/menu.rs @@ -0,0 +1,102 @@ +use crate::backend::Backend; +use crate::{Primitive, Renderer}; +use iced_native::{ + layer, mouse, Background, Color, Font, HorizontalAlignment, Point, + Rectangle, VerticalAlignment, +}; + +impl layer::menu::Renderer for Renderer +where + B: Backend, +{ + fn decorate( + &mut self, + bounds: Rectangle, + _cursor_position: Point, + (primitives, mouse_cursor): Self::Output, + ) -> Self::Output { + ( + Primitive::Group { + primitives: vec![ + Primitive::Quad { + bounds, + background: Background::Color( + [0.87, 0.87, 0.87].into(), + ), + border_color: [0.7, 0.7, 0.7].into(), + border_width: 1, + border_radius: 0, + }, + primitives, + ], + }, + mouse_cursor, + ) + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + options: &[T], + hovered_option: Option, + text_size: u16, + padding: u16, + ) -> Self::Output { + use std::f32; + + let is_mouse_over = bounds.contains(cursor_position); + + let mut primitives = Vec::new(); + + for (i, option) in options.iter().enumerate() { + let is_selected = hovered_option == Some(i); + + let bounds = Rectangle { + x: bounds.x, + y: bounds.y + + ((text_size as usize + padding as usize * 2) * i) as f32, + width: bounds.width, + height: f32::from(text_size + padding * 2), + }; + + if is_selected { + primitives.push(Primitive::Quad { + bounds, + background: Background::Color([0.4, 0.4, 1.0].into()), + border_color: Color::TRANSPARENT, + border_width: 0, + border_radius: 0, + }); + } + + primitives.push(Primitive::Text { + content: option.to_string(), + bounds: Rectangle { + x: bounds.x + f32::from(padding), + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }, + size: f32::from(text_size), + font: Font::Default, + color: if is_selected { + Color::WHITE + } else { + Color::BLACK + }, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }); + } + + ( + Primitive::Group { primitives }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 38d8dffa..bad35ccf 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -2,7 +2,7 @@ //! for [`iced`]. //! //! [`iced`]: https://github.com/hecrj/iced -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![deny(unsafe_code)] diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 1f6d6559..a0d06999 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -9,6 +9,7 @@ //! ``` pub mod button; pub mod checkbox; +pub mod combo_box; pub mod container; pub mod image; pub mod pane_grid; diff --git a/graphics/src/widget/combo_box.rs b/graphics/src/widget/combo_box.rs new file mode 100644 index 00000000..27ea762a --- /dev/null +++ b/graphics/src/widget/combo_box.rs @@ -0,0 +1,67 @@ +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, Background, Color, Font, HorizontalAlignment, Point, Rectangle, + VerticalAlignment, +}; + +pub use iced_native::ComboBox; + +impl iced_native::combo_box::Renderer for Renderer +where + B: Backend + backend::Text, +{ + const DEFAULT_PADDING: u16 = 5; + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option, + text_size: u16, + padding: u16, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let background = Primitive::Quad { + bounds, + background: Background::Color([0.87, 0.87, 0.87].into()), + border_color: if is_mouse_over { + Color::BLACK + } else { + [0.7, 0.7, 0.7].into() + }, + border_width: 1, + border_radius: 0, + }; + + ( + if let Some(label) = selected { + let label = Primitive::Text { + content: label, + size: f32::from(text_size), + font: Font::Default, + color: Color::BLACK, + bounds: Rectangle { + x: bounds.x + f32::from(padding), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + Primitive::Group { + primitives: vec![background, label], + } + } else { + background + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/native/src/element.rs b/native/src/element.rs index 01379d2d..c881871a 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -2,6 +2,7 @@ use crate::{ layout, Clipboard, Color, Event, Hasher, Layout, Length, Overlay, Point, Widget, }; +use std::rc::Rc; /// A generic [`Widget`]. /// @@ -282,7 +283,7 @@ where struct Map<'a, A, B, Renderer> { widget: Box + 'a>, - mapper: Box B>, + mapper: Rc B>, } impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { @@ -295,14 +296,16 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { { Map { widget, - mapper: Box::new(mapper), + mapper: Rc::new(mapper), } } } impl<'a, A, B, Renderer> Widget<'a, B, Renderer> for Map<'a, A, B, Renderer> where - Renderer: crate::Renderer, + Renderer: crate::Renderer + 'a, + A: 'static, + B: 'static, { fn width(&self) -> Length { self.widget.width() @@ -359,6 +362,15 @@ where fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option> { + self.widget + .overlay(layout) + .map(|overlay| overlay.map(self.mapper.clone())) + } } struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -434,4 +446,11 @@ where fn hash_layout(&self, state: &mut Hasher) { self.element.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option> { + self.element.overlay(layout) + } } diff --git a/native/src/layer.rs b/native/src/layer.rs index d89fb4e5..eacfe94b 100644 --- a/native/src/layer.rs +++ b/native/src/layer.rs @@ -1,3 +1,8 @@ +pub mod menu; + +#[doc(no_inline)] +pub use menu::Menu; + use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size}; pub trait Layer diff --git a/native/src/layer/menu.rs b/native/src/layer/menu.rs new file mode 100644 index 00000000..9e26767b --- /dev/null +++ b/native/src/layer/menu.rs @@ -0,0 +1,305 @@ +use crate::{ + container, layout, mouse, scrollable, Clipboard, Container, Element, Event, + Hasher, Layer, Layout, Length, Point, Rectangle, Scrollable, Size, Widget, +}; +use std::borrow::Cow; + +pub struct Menu<'a, Message, Renderer: self::Renderer> { + container: Container<'a, Message, Renderer>, + is_open: &'a mut bool, + width: u16, +} + +#[derive(Default)] +pub struct State { + scrollable: scrollable::State, + hovered_option: Option, + is_open: bool, +} + +impl State { + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn open(&mut self, hovered_option: Option) { + self.is_open = true; + self.hovered_option = hovered_option; + } +} + +impl<'a, Message, Renderer: self::Renderer> Menu<'a, Message, Renderer> +where + Message: 'static, + Renderer: 'a, +{ + pub fn new( + state: &'a mut State, + options: impl Into>, + on_selected: Box Message>, + width: u16, + text_size: u16, + padding: u16, + ) -> Self + where + T: Clone + ToString, + [T]: ToOwned>, + { + let container = Container::new( + Scrollable::new(&mut state.scrollable).push(List::new( + &mut state.hovered_option, + options, + on_selected, + text_size, + padding, + )), + ) + .padding(1); + + Self { + container, + is_open: &mut state.is_open, + width, + } + } +} + +impl<'a, Message, Renderer> Layer + for Menu<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let limits = layout::Limits::new( + Size::ZERO, + Size::new(bounds.width - position.x, bounds.height - position.y), + ) + .width(Length::Units(self.width)); + + let mut node = self.container.layout(renderer, &limits); + + node.move_to(position); + + node + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + use std::hash::Hash; + + (position.x as u32).hash(state); + (position.y as u32).hash(state); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + let current_messages = messages.len(); + + self.container.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ); + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + if !bounds.contains(cursor_position) + || current_messages < messages.len() => + { + *self.is_open = false; + } + _ => {} + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + let primitives = + self.container + .draw(renderer, defaults, layout, cursor_position); + + renderer.decorate(layout.bounds(), cursor_position, primitives) + } +} + +struct List<'a, T, Message> +where + [T]: ToOwned, +{ + hovered_option: &'a mut Option, + options: Cow<'a, [T]>, + on_selected: Box Message>, + text_size: u16, + padding: u16, +} + +impl<'a, T, Message> List<'a, T, Message> +where + [T]: ToOwned, +{ + pub fn new( + hovered_option: &'a mut Option, + options: impl Into>, + on_selected: Box Message>, + text_size: u16, + padding: u16, + ) -> Self { + List { + hovered_option, + options: options.into(), + on_selected, + text_size, + padding, + } + } +} + +impl<'a, T, Message, Renderer> Widget<'a, Message, Renderer> + for List<'a, T, Message> +where + T: ToString + Clone, + [T]: ToOwned, + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits.width(Length::Fill).height(Length::Shrink); + + let size = { + let intrinsic = Size::new( + 0.0, + f32::from(self.text_size + self.padding * 2) + * self.options.len() as f32, + ); + + limits.resolve(intrinsic) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + 0.hash(state); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + messages.push((self.on_selected)(option.clone())); + } + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / f32::from(self.text_size + self.padding * 2)) + as usize, + ); + } + } + _ => {} + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + &self.options, + *self.hovered_option, + self.text_size, + self.padding, + ) + } +} + +pub trait Renderer: scrollable::Renderer + container::Renderer { + fn decorate( + &mut self, + bounds: Rectangle, + cursor_position: Point, + primitive: Self::Output, + ) -> Self::Output; + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + options: &[T], + hovered_option: Option, + text_size: u16, + padding: u16, + ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into> + for List<'a, T, Message> +where + T: ToString + Clone, + [T]: ToOwned, + Message: 'static, + Renderer: self::Renderer, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 6974c2bd..99d80126 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -31,11 +31,12 @@ //! [`UserInterface`]: struct.UserInterface.html //! [renderer]: renderer/index.html //#![deny(missing_docs)] -#![deny(missing_debug_implementations)] +//#![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] pub mod keyboard; +pub mod layer; pub mod layout; pub mod mouse; pub mod program; @@ -48,7 +49,6 @@ mod clipboard; mod element; mod event; mod hasher; -mod layer; mod overlay; mod runtime; mod user_interface; diff --git a/native/src/overlay.rs b/native/src/overlay.rs index a4bd5ea3..d7a1e082 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -1,4 +1,5 @@ use crate::{layout, Clipboard, Event, Hasher, Layer, Layout, Point, Size}; +use std::rc::Rc; #[allow(missing_debug_implementations)] pub struct Overlay<'a, Message, Renderer> { @@ -17,6 +18,18 @@ where Self { position, layer } } + pub fn map(self, f: Rc B>) -> Overlay<'a, B, Renderer> + where + Message: 'static, + Renderer: 'a, + B: 'static, + { + Overlay { + position: self.position, + layer: Box::new(Map::new(self.layer, f)), + } + } + pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { self.layer.layout(renderer, bounds, self.position) } @@ -37,12 +50,87 @@ where pub fn on_event( &mut self, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _messages: &mut Vec, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { + self.layer.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } +} + +struct Map<'a, A, B, Renderer> { + layer: Box + 'a>, + mapper: Rc B>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + layer: Box + 'a>, + mapper: Rc B + 'static>, + ) -> Map<'a, A, B, Renderer> { + Map { layer, mapper } + } +} + +impl<'a, A, B, Renderer> Layer for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.layer.layout(renderer, bounds, position) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) { + let mut original_messages = Vec::new(); + + self.layer.on_event( + event, + layout, + cursor_position, + &mut original_messages, + renderer, + clipboard, + ); + + original_messages + .drain(..) + .for_each(|message| messages.push((self.mapper)(message))); + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.layer.draw(renderer, defaults, layout, cursor_position) + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + self.layer.hash_layout(state, position); } } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 6758bce3..12cea684 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -217,6 +217,14 @@ where for event in events { if let Some(overlay) = &mut self.overlay { + let base_cursor = + if overlay.layout.bounds().contains(cursor_position) { + // TODO: Encode cursor availability + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + overlay.root.on_event( event.clone(), Layout::new(&overlay.layout), @@ -226,14 +234,6 @@ where clipboard, ); - let base_cursor = - if overlay.layout.bounds().contains(cursor_position) { - // TODO: Encode cursor availability - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - self.base.root.widget.on_event( event, Layout::new(&self.base.layout), diff --git a/native/src/widget.rs b/native/src/widget.rs index 0494636f..664a0cfd 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -23,6 +23,7 @@ pub mod button; pub mod checkbox; pub mod column; +pub mod combo_box; pub mod container; pub mod image; pub mod pane_grid; @@ -43,6 +44,8 @@ pub use checkbox::Checkbox; #[doc(no_inline)] pub use column::Column; #[doc(no_inline)] +pub use combo_box::ComboBox; +#[doc(no_inline)] pub use container::Container; #[doc(no_inline)] pub use image::Image; diff --git a/native/src/widget/combo_box.rs b/native/src/widget/combo_box.rs new file mode 100644 index 00000000..0b25b836 --- /dev/null +++ b/native/src/widget/combo_box.rs @@ -0,0 +1,256 @@ +use crate::{ + layer::{self, menu}, + layout, mouse, scrollable, text, Clipboard, Element, Event, Hasher, Layout, + Length, Overlay, Point, Rectangle, Size, Vector, Widget, +}; +use std::borrow::Cow; + +pub struct ComboBox<'a, T, Message> +where + [T]: ToOwned>, +{ + internal: Option>, + options: Cow<'a, [T]>, + selected: Option, + width: Length, + padding: u16, + text_size: Option, +} + +#[derive(Default)] +pub struct State { + menu: menu::State, +} + +pub struct Internal<'a, T, Message> { + menu: &'a mut menu::State, + on_selected: Box Message>, +} + +impl<'a, T: 'a, Message> ComboBox<'a, T, Message> +where + T: ToString, + [T]: ToOwned>, +{ + pub fn new( + state: &'a mut State, + options: impl Into>, + selected: Option, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + Self { + internal: Some(Internal { + menu: &mut state.menu, + on_selected: Box::new(on_selected), + }), + options: options.into(), + selected, + width: Length::Shrink, + text_size: None, + padding: 5, + } + } + + /// Sets the width of the [`ComboBox`]. + /// + /// [`ComboBox`]: struct.Button.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`ComboBox`]. + /// + /// [`ComboBox`]: struct.Button.html + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<'a, Message, Renderer> + for ComboBox<'a, T, Message> +where + T: Clone + ToString + Eq, + [T]: ToOwned>, + Message: 'static, + Renderer: self::Renderer + scrollable::Renderer + 'a, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits + .width(self.width) + .height(Length::Shrink) + .pad(f32::from(self.padding)); + + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let max_width = match self.width { + Length::Shrink => { + let labels = self.options.iter().map(ToString::to_string); + + labels + .map(|label| { + let (width, _) = renderer.measure( + &label, + text_size, + Renderer::Font::default(), + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }) + .max() + .unwrap_or(100) + } + _ => 0, + }; + + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size), + f32::from(text_size), + ); + + limits.resolve(intrinsic).pad(f32::from(self.padding)) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + match self.width { + Length::Shrink => { + self.options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + self.width.hash(state); + } + } + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if let Some(internal) = &mut self.internal { + if layout.bounds().contains(cursor_position) { + let selected = self.selected.as_ref(); + + internal.menu.open( + self.options + .iter() + .position(|option| Some(option) == selected), + ); + } + } + } + _ => {} + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + self.selected.as_ref().map(ToString::to_string), + self.text_size.unwrap_or(renderer.default_size()), + self.padding, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option> { + let is_open = self + .internal + .as_ref() + .map(|internal| internal.menu.is_open()) + .unwrap_or(false); + + if is_open { + if let Some(Internal { menu, on_selected }) = self.internal.take() { + Some(Overlay::new( + layout.position() + + Vector::new(0.0, layout.bounds().height), + Box::new(layer::Menu::new( + menu, + self.options.clone(), + on_selected, + layout.bounds().width.round() as u16, + self.text_size.unwrap_or(20), + self.padding, + )), + )) + } else { + None + } + } else { + None + } + } +} + +pub trait Renderer: text::Renderer + menu::Renderer { + const DEFAULT_PADDING: u16; + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option, + text_size: u16, + padding: u16, + ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into> + for ComboBox<'a, T, Message> +where + T: Clone + ToString + Eq, + [T]: ToOwned>, + Renderer: self::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index d7ad98e6..25fd8982 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -113,7 +113,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<'a, Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer + column::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { Widget::::width(&self.content) @@ -454,7 +454,7 @@ pub struct Scroller { /// /// [`Scrollable`]: struct.Scrollable.html /// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +pub trait Renderer: column::Renderer + Sized { /// The style supported by this renderer. type Style: Default; @@ -502,7 +502,7 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + column::Renderer, + Renderer: 'a + self::Renderer, Message: 'a, { fn from( diff --git a/src/widget.rs b/src/widget.rs index 007bd531..034f02cd 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -19,7 +19,7 @@ #[cfg(not(target_arch = "wasm32"))] mod platform { pub use crate::renderer::widget::{ - button, checkbox, container, pane_grid, progress_bar, radio, + button, checkbox, combo_box, container, pane_grid, progress_bar, radio, scrollable, slider, text_input, Column, Row, Space, Text, }; @@ -44,10 +44,10 @@ mod platform { #[doc(no_inline)] pub use { - button::Button, checkbox::Checkbox, container::Container, image::Image, - pane_grid::PaneGrid, progress_bar::ProgressBar, radio::Radio, - scrollable::Scrollable, slider::Slider, svg::Svg, - text_input::TextInput, + button::Button, checkbox::Checkbox, combo_box::ComboBox, + container::Container, image::Image, pane_grid::PaneGrid, + progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable, + slider::Slider, svg::Svg, text_input::TextInput, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index e51a225c..0186b007 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -20,7 +20,7 @@ //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs //! [WebGPU API]: https://gpuweb.github.io/gpuweb/ //! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index d17b7a5d..0f390c8d 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -11,6 +11,7 @@ use crate::Renderer; pub mod button; pub mod checkbox; +pub mod combo_box; pub mod container; pub mod pane_grid; pub mod progress_bar; @@ -24,6 +25,8 @@ pub use button::Button; #[doc(no_inline)] pub use checkbox::Checkbox; #[doc(no_inline)] +pub use combo_box::ComboBox; +#[doc(no_inline)] pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; diff --git a/wgpu/src/widget/combo_box.rs b/wgpu/src/widget/combo_box.rs new file mode 100644 index 00000000..bb3931ef --- /dev/null +++ b/wgpu/src/widget/combo_box.rs @@ -0,0 +1,3 @@ +pub use iced_native::combo_box::State; + +pub type ComboBox<'a, T, Message> = iced_native::ComboBox<'a, T, Message>;