diff options
46 files changed, 2153 insertions, 85 deletions
@@ -64,6 +64,7 @@ members = [ "examples/geometry", "examples/integration", "examples/pane_grid", + "examples/pick_list", "examples/pokedex", "examples/progress_bar", "examples/solar_system", diff --git a/examples/README.md b/examples/README.md index 8e1b781f..34a916a1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -100,6 +100,7 @@ A bunch of simpler examples exist: - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). - [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application. - [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized. +- [`pick_list`](pick_list), a dropdown list of selectable options. - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 080d55c0..27d4eec1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,15 +1,19 @@ //! This example showcases an interactive version of the Game of Life, invented //! by John Conway. It leverages a `Canvas` together with other widgets. +mod preset; mod style; use grid::Grid; +use iced::button::{self, Button}; +use iced::executor; +use iced::pick_list::{self, PickList}; +use iced::slider::{self, Slider}; +use iced::time; use iced::{ - button::{self, Button}, - executor, - slider::{self, Slider}, - time, Align, Application, Checkbox, Column, Command, Container, Element, - Length, Row, Settings, Subscription, Text, + Align, Application, Checkbox, Column, Command, Container, Element, Length, + Row, Settings, Subscription, Text, }; +use preset::Preset; use std::time::{Duration, Instant}; pub fn main() { @@ -38,6 +42,7 @@ enum Message { Next, Clear, SpeedChanged(f32), + PresetPicked(Preset), } impl Application for GameOfLife { @@ -48,7 +53,7 @@ impl Application for GameOfLife { fn new(_flags: ()) -> (Self, Command<Message>) { ( Self { - speed: 1, + speed: 5, ..Self::default() }, Command::none(), @@ -93,6 +98,9 @@ impl Application for GameOfLife { self.speed = speed.round() as usize; } } + Message::PresetPicked(new_preset) => { + self.grid = Grid::from_preset(new_preset); + } } Command::none() @@ -113,6 +121,7 @@ impl Application for GameOfLife { self.is_playing, self.grid.are_lines_visible(), selected_speed, + self.grid.preset(), ); let content = Column::new() @@ -128,6 +137,7 @@ impl Application for GameOfLife { } mod grid { + use crate::Preset; use iced::{ canvas::{ self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, @@ -142,6 +152,7 @@ mod grid { pub struct Grid { state: State, + preset: Preset, interaction: Interaction, life_cache: Cache, grid_cache: Cache, @@ -171,8 +182,24 @@ mod grid { impl Default for Grid { fn default() -> Self { + Self::from_preset(Preset::default()) + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn from_preset(preset: Preset) -> Self { Self { - state: State::default(), + state: State::with_life( + preset + .life() + .into_iter() + .map(|(i, j)| Cell { i, j }) + .collect(), + ), + preset, interaction: Interaction::None, life_cache: Cache::default(), grid_cache: Cache::default(), @@ -184,11 +211,6 @@ mod grid { version: 0, } } - } - - impl Grid { - const MIN_SCALING: f32 = 0.1; - const MAX_SCALING: f32 = 2.0; pub fn tick( &mut self, @@ -217,10 +239,14 @@ mod grid { Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); + + self.preset = Preset::Custom; } Message::Unpopulate(cell) => { self.state.unpopulate(&cell); self.life_cache.clear(); + + self.preset = Preset::Custom; } Message::Ticked { result: Ok(life), @@ -230,6 +256,7 @@ mod grid { self.state.update(life); self.life_cache.clear(); + self.version += 1; self.last_tick_duration = tick_duration; } Message::Ticked { @@ -250,11 +277,16 @@ mod grid { pub fn clear(&mut self) { self.state = State::default(); + self.preset = Preset::Custom; self.version += 1; self.life_cache.clear(); } + pub fn preset(&self) -> Preset { + self.preset + } + pub fn toggle_lines(&mut self, enabled: bool) { self.show_lines = enabled; } @@ -533,6 +565,13 @@ mod grid { } impl State { + pub fn with_life(life: Life) -> Self { + Self { + life, + ..Self::default() + } + } + fn cell_count(&self) -> usize { self.life.len() + self.births.len() } @@ -647,6 +686,14 @@ mod grid { } } + impl std::iter::FromIterator<Cell> for Life { + fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self { + Life { + cells: iter.into_iter().collect(), + } + } + } + impl std::fmt::Debug for Life { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Life") @@ -741,6 +788,7 @@ struct Controls { next_button: button::State, clear_button: button::State, speed_slider: slider::State, + preset_list: pick_list::State, } impl Controls { @@ -749,6 +797,7 @@ impl Controls { is_playing: bool, is_grid_enabled: bool, speed: usize, + preset: Preset, ) -> Element<'a, Message> { let playback_controls = Row::new() .spacing(10) @@ -794,6 +843,17 @@ impl Controls { .text_size(16), ) .push( + PickList::new( + &mut self.preset_list, + preset::ALL, + Some(preset), + Message::PresetPicked, + ) + .padding(8) + .text_size(16) + .style(style::PickList), + ) + .push( Button::new(&mut self.clear_button, Text::new("Clear")) .on_press(Message::Clear) .style(style::Clear), diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs new file mode 100644 index 00000000..05157b6a --- /dev/null +++ b/examples/game_of_life/src/preset.rs @@ -0,0 +1,142 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Preset { + Custom, + XKCD, + Glider, + SmallExploder, + Exploder, + TenCellRow, + LightweightSpaceship, + Tumbler, + GliderGun, + Acorn, +} + +pub static ALL: &[Preset] = &[ + Preset::Custom, + Preset::XKCD, + Preset::Glider, + Preset::SmallExploder, + Preset::Exploder, + Preset::TenCellRow, + Preset::LightweightSpaceship, + Preset::Tumbler, + Preset::GliderGun, + Preset::Acorn, +]; + +impl Preset { + pub fn life(self) -> Vec<(isize, isize)> { + #[rustfmt::skip] + let cells = match self { + Preset::Custom => vec![], + Preset::XKCD => vec![ + " xxx ", + " x x ", + " x x ", + " x ", + "x xxx ", + " x x x ", + " x x", + " x x ", + " x x ", + ], + Preset::Glider => vec![ + " x ", + " x", + "xxx" + ], + Preset::SmallExploder => vec![ + " x ", + "xxx", + "x x", + " x ", + ], + Preset::Exploder => vec![ + "x x x", + "x x", + "x x", + "x x", + "x x x", + ], + Preset::TenCellRow => vec![ + "xxxxxxxxxx", + ], + Preset::LightweightSpaceship => vec![ + " xxxxx", + "x x", + " x", + "x x ", + ], + Preset::Tumbler => vec![ + " xx xx ", + " xx xx ", + " x x ", + "x x x x", + "x x x x", + "xx xx", + ], + Preset::GliderGun => vec![ + " x ", + " x x ", + " xx xx xx", + " x x xx xx", + "xx x x xx ", + "xx x x xx x x ", + " x x x ", + " x x ", + " xx ", + ], + Preset::Acorn => vec![ + " x ", + " x ", + "xx xxx", + ], + }; + + let start_row = -(cells.len() as isize / 2); + + cells + .into_iter() + .enumerate() + .flat_map(|(i, cells)| { + let start_column = -(cells.len() as isize / 2); + + cells + .chars() + .enumerate() + .filter(|(_, c)| !c.is_whitespace()) + .map(move |(j, _)| { + (start_row + i as isize, start_column + j as isize) + }) + }) + .collect() + } +} + +impl Default for Preset { + fn default() -> Preset { + Preset::XKCD + } +} + +impl std::fmt::Display for Preset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Preset::Custom => "Custom", + Preset::XKCD => "xkcd #2293", + Preset::Glider => "Glider", + Preset::SmallExploder => "Small Exploder", + Preset::Exploder => "Exploder", + Preset::TenCellRow => "10 Cell Row", + Preset::LightweightSpaceship => "Lightweight spaceship", + Preset::Tumbler => "Tumbler", + Preset::GliderGun => "Gosper Glider Gun", + Preset::Acorn => "Acorn", + } + ) + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs index d59569f2..308ce43c 100644 --- a/examples/game_of_life/src/style.rs +++ b/examples/game_of_life/src/style.rs @@ -1,4 +1,4 @@ -use iced::{button, container, slider, Background, Color}; +use iced::{button, container, pick_list, slider, Background, Color}; const ACTIVE: Color = Color::from_rgb( 0x72 as f32 / 255.0, @@ -18,6 +18,12 @@ const HOVERED: Color = Color::from_rgb( 0xC4 as f32 / 255.0, ); +const BACKGROUND: Color = Color::from_rgb( + 0x2F as f32 / 255.0, + 0x31 as f32 / 255.0, + 0x36 as f32 / 255.0, +); + pub struct Container; impl container::StyleSheet for Container { @@ -132,3 +138,51 @@ impl slider::StyleSheet for Slider { } } } + +pub struct PickList; + +impl pick_list::StyleSheet for PickList { + fn menu(&self) -> pick_list::Menu { + pick_list::Menu { + text_color: Color::WHITE, + background: BACKGROUND.into(), + border_width: 1, + border_color: Color { + a: 0.7, + ..Color::BLACK + }, + selected_background: Color { + a: 0.5, + ..Color::BLACK + } + .into(), + selected_text_color: Color::WHITE, + } + } + + fn active(&self) -> pick_list::Style { + pick_list::Style { + text_color: Color::WHITE, + background: BACKGROUND.into(), + border_width: 1, + border_color: Color { + a: 0.6, + ..Color::BLACK + }, + border_radius: 2, + icon_size: 0.5, + } + } + + fn hovered(&self) -> pick_list::Style { + let active = self.active(); + + pick_list::Style { + border_color: Color { + a: 0.9, + ..Color::BLACK + }, + ..active + } + } +} diff --git a/examples/pick_list/Cargo.toml b/examples/pick_list/Cargo.toml new file mode 100644 index 00000000..a87d7217 --- /dev/null +++ b/examples/pick_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pick_list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } diff --git a/examples/pick_list/README.md b/examples/pick_list/README.md new file mode 100644 index 00000000..6dc80bf4 --- /dev/null +++ b/examples/pick_list/README.md @@ -0,0 +1,18 @@ +## Pick-list + +A dropdown list of selectable options. + +It displays and positions an overlay based on the window position of the widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package pick_list +``` + +[`main`]: src/main.rs diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs new file mode 100644 index 00000000..66ed6c6f --- /dev/null +++ b/examples/pick_list/src/main.rs @@ -0,0 +1,113 @@ +use iced::{ + pick_list, scrollable, Align, Container, Element, Length, PickList, + Sandbox, Scrollable, Settings, Space, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + scroll: scrollable::State, + pick_list: pick_list::State, + selected_language: Language, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + LanguageSelected(Language), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Pick list - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::LanguageSelected(language) => { + self.selected_language = language; + } + } + } + + fn view(&mut self) -> Element<Message> { + let pick_list = PickList::new( + &mut self.pick_list, + &Language::ALL[..], + Some(self.selected_language), + Message::LanguageSelected, + ); + + let mut content = Scrollable::new(&mut self.scroll) + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push(Space::with_height(Length::Units(600))) + .push(Text::new("Which is your favorite language?")) + .push(pick_list); + + content = content.push(Space::with_height(Length::Units(600))); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .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/examples/todos/src/main.rs b/examples/todos/src/main.rs index c9cbcc69..5713a6f2 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -425,7 +425,7 @@ impl Filter { } } -fn loading_message() -> Element<'static, Message> { +fn loading_message<'a>() -> Element<'a, Message> { Container::new( Text::new("Loading...") .horizontal_alignment(HorizontalAlignment::Center) @@ -437,7 +437,7 @@ fn loading_message() -> Element<'static, Message> { .into() } -fn empty_message(message: &str) -> Element<'static, Message> { +fn empty_message<'a>(message: &str) -> Element<'a, Message> { Container::new( Text::new(message) .width(Length::Fill) diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 8b5b4f9c..e1685816 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -193,6 +193,7 @@ impl iced_graphics::Backend for Backend { impl backend::Text for Backend { const ICON_FONT: Font = font::ICONS; const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; fn default_size(&self) -> u16 { self.default_text_size diff --git a/glow/src/widget.rs b/glow/src/widget.rs index 9968092b..4e2fedc5 100644 --- a/glow/src/widget.rs +++ b/glow/src/widget.rs @@ -13,6 +13,7 @@ pub mod button; pub mod checkbox; pub mod container; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod scrollable; @@ -28,6 +29,8 @@ pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/glow/src/widget/pick_list.rs b/glow/src/widget/pick_list.rs new file mode 100644 index 00000000..fccc68c9 --- /dev/null +++ b/glow/src/widget/pick_list.rs @@ -0,0 +1,9 @@ +//! Display a dropdown list of selectable values. +pub use iced_native::pick_list::State; + +pub use iced_graphics::overlay::menu::Style as Menu; +pub use iced_graphics::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message> = + iced_native::PickList<'a, T, Message, crate::Renderer>; diff --git a/graphics/fonts/Icons.ttf b/graphics/fonts/Icons.ttf Binary files differindex 1c832f86..5e455b69 100644 --- a/graphics/fonts/Icons.ttf +++ b/graphics/fonts/Icons.ttf diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index b73c636e..dd7dbbc2 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -22,9 +22,14 @@ pub trait Text { /// The `char` representing a ✔ icon in the [`ICON_FONT`]. /// - /// [`ICON_FONT`]: #associatedconst.ICON_FONt + /// [`ICON_FONT`]: #associatedconst.ICON_FONT const CHECKMARK_ICON: char; + /// The `char` representing a ▼ icon in the built-in [`ICONS`] font. + /// + /// [`ICON_FONT`]: #associatedconst.ICON_FONT + const ARROW_DOWN_ICON: char; + /// Returns the default size of text. fn default_size(&self) -> u16; diff --git a/graphics/src/font.rs b/graphics/src/font.rs index bcc28857..5c62681c 100644 --- a/graphics/src/font.rs +++ b/graphics/src/font.rs @@ -31,3 +31,9 @@ pub const ICONS: iced_native::Font = iced_native::Font::External { #[cfg(feature = "font-icons")] #[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] pub const CHECKMARK_ICON: char = '\u{F00C}'; + +/// The `char` representing a ▼ icon in the built-in [`ICONS`] font. +/// +/// [`ICONS`]: const.ICONS.html +#[cfg(feature = "font-icons")] +pub const ARROW_DOWN_ICON: char = '\u{E800}'; diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 38d8dffa..d03f3b48 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -13,13 +13,14 @@ mod primitive; mod renderer; mod transformation; mod viewport; -mod widget; pub mod backend; pub mod defaults; pub mod font; pub mod layer; +pub mod overlay; pub mod triangle; +pub mod widget; pub mod window; #[doc(no_inline)] diff --git a/graphics/src/overlay.rs b/graphics/src/overlay.rs new file mode 100644 index 00000000..bc0ed744 --- /dev/null +++ b/graphics/src/overlay.rs @@ -0,0 +1,2 @@ +//! Display interactive elements on top of other widgets. +pub mod menu; diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs new file mode 100644 index 00000000..a952f065 --- /dev/null +++ b/graphics/src/overlay/menu.rs @@ -0,0 +1,108 @@ +//! Build and show dropdown menus. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, overlay, Color, Font, HorizontalAlignment, Point, Rectangle, + VerticalAlignment, +}; + +pub use iced_style::menu::Style; + +impl<B> overlay::menu::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Style; + + fn decorate( + &mut self, + bounds: Rectangle, + _cursor_position: Point, + style: &Style, + (primitives, mouse_cursor): Self::Output, + ) -> Self::Output { + ( + Primitive::Group { + primitives: vec![ + Primitive::Quad { + bounds, + background: style.background, + border_color: style.border_color, + border_width: style.border_width, + border_radius: 0, + }, + primitives, + ], + }, + mouse_cursor, + ) + } + + fn draw<T: ToString>( + &mut self, + bounds: Rectangle, + cursor_position: Point, + options: &[T], + hovered_option: Option<usize>, + padding: u16, + text_size: u16, + font: Font, + style: &Style, + ) -> 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: style.selected_background, + 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, + color: if is_selected { + style.selected_text_color + } else { + style.text_color + }, + 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/renderer.rs b/graphics/src/renderer.rs index c9360f3a..5d51e6d4 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,7 +1,9 @@ use crate::{Backend, Defaults, Primitive}; use iced_native::layout::{self, Layout}; use iced_native::mouse; -use iced_native::{Background, Color, Element, Point, Widget}; +use iced_native::{ + Background, Color, Element, Point, Rectangle, Vector, Widget, +}; /// A backend-agnostic renderer that supports all the built-in widgets. #[derive(Debug)] @@ -53,6 +55,35 @@ where layout } + + fn overlay( + &mut self, + (base_primitive, base_cursor): (Primitive, mouse::Interaction), + (overlay_primitives, overlay_cursor): (Primitive, mouse::Interaction), + overlay_bounds: Rectangle, + ) -> (Primitive, mouse::Interaction) { + ( + Primitive::Group { + primitives: vec![ + base_primitive, + Primitive::Clip { + bounds: Rectangle { + width: overlay_bounds.width + 0.5, + height: overlay_bounds.height + 0.5, + ..overlay_bounds + }, + offset: Vector::new(0, 0), + content: Box::new(overlay_primitives), + }, + ], + }, + if base_cursor > overlay_cursor { + base_cursor + } else { + overlay_cursor + }, + ) + } } impl<B> layout::Debugger for Renderer<B> diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 1f6d6559..94a65011 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -12,6 +12,7 @@ pub mod checkbox; pub mod container; pub mod image; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod scrollable; @@ -33,6 +34,8 @@ pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs new file mode 100644 index 00000000..f42a8707 --- /dev/null +++ b/graphics/src/widget/pick_list.rs @@ -0,0 +1,97 @@ +//! Display a dropdown list of selectable values. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, Font, HorizontalAlignment, Point, Rectangle, VerticalAlignment, +}; +use iced_style::menu; + +pub use iced_native::pick_list::State; +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message, Backend> = + iced_native::PickList<'a, T, Message, Renderer<Backend>>; + +impl<B> iced_native::pick_list::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Box<dyn StyleSheet>; + + const DEFAULT_PADDING: u16 = 5; + + fn menu_style(style: &Box<dyn StyleSheet>) -> menu::Style { + style.menu() + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Font, + style: &Box<dyn StyleSheet>, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if is_mouse_over { + style.hovered() + } else { + style.active() + }; + + let background = Primitive::Quad { + bounds, + background: style.background, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }; + + let arrow_down = Primitive::Text { + content: B::ARROW_DOWN_ICON.to_string(), + font: B::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding) * 2.0, + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Center, + }; + + ( + Primitive::Group { + primitives: if let Some(label) = selected { + let label = Primitive::Text { + content: label, + size: f32::from(text_size), + font, + color: style.text_color, + bounds: Rectangle { + x: bounds.x + f32::from(padding), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + vec![background, label, arrow_down] + } else { + vec![background, arrow_down] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/native/src/element.rs b/native/src/element.rs index 73e39012..514a135b 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,5 +1,6 @@ use crate::{ - layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget, + layout, overlay, Clipboard, Color, Event, Hasher, Layout, Length, Point, + Widget, }; /// A generic [`Widget`]. @@ -22,7 +23,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> where Renderer: crate::Renderer, { - /// Create a new [`Element`] containing the given [`Widget`]. + /// Creates a new [`Element`] containing the given [`Widget`]. /// /// [`Element`]: struct.Element.html /// [`Widget`]: widget/trait.Widget.html @@ -270,6 +271,16 @@ where pub fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + /// Returns the overlay of the [`Element`], if there is any. + /// + /// [`Element`]: struct.Element.html + pub fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.widget.overlay(layout) + } } struct Map<'a, A, B, Renderer> { @@ -294,7 +305,9 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget<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() @@ -351,6 +364,17 @@ where fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, B, Renderer>> { + let mapper = &self.mapper; + + self.widget + .overlay(layout) + .map(move |overlay| overlay.map(mapper)) + } } struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -426,4 +450,11 @@ where fn hash_layout(&self, state: &mut Hasher) { self.element.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.element.overlay(layout) + } } diff --git a/native/src/lib.rs b/native/src/lib.rs index b67ff2a1..067e3c0a 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -38,6 +38,7 @@ pub mod keyboard; pub mod layout; pub mod mouse; +pub mod overlay; pub mod program; pub mod renderer; pub mod subscription; @@ -75,6 +76,7 @@ pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; +pub use overlay::Overlay; pub use program::Program; pub use renderer::Renderer; pub use runtime::Runtime; diff --git a/native/src/overlay.rs b/native/src/overlay.rs new file mode 100644 index 00000000..7c3bec32 --- /dev/null +++ b/native/src/overlay.rs @@ -0,0 +1,84 @@ +//! Display interactive elements on top of other widgets. +mod element; + +pub mod menu; + +pub use element::Element; +pub use menu::Menu; + +use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay<Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Returns the layout [`Node`] of the [`Overlay`]. + /// + /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + /// + /// [`Node`]: ../layout/struct.Node.html + /// [`Widget`]: trait.Overlay.html + /// [`Layout`]: ../layout/struct.Layout.html + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node; + + /// Draws the [`Overlay`] using the associated `Renderer`. + /// + /// [`Overlay`]: trait.Overlay.html + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output; + + /// Computes the _layout_ hash of the [`Overlay`]. + /// + /// The produced hash is used by the runtime to decide if the [`Layout`] + /// needs to be recomputed between frames. Therefore, to ensure maximum + /// efficiency, the hash should only be affected by the properties of the + /// [`Overlay`] that can affect layouting. + /// + /// For example, the [`Text`] widget does not hash its color property, as + /// its value cannot affect the overall [`Layout`] of the user interface. + /// + /// [`Overlay`]: trait.Overlay.html + /// [`Layout`]: ../layout/struct.Layout.html + /// [`Text`]: text/struct.Text.html + fn hash_layout(&self, state: &mut Hasher, position: Point); + + /// Processes a runtime [`Event`]. + /// + /// It receives: + /// * an [`Event`] describing user interaction + /// * the computed [`Layout`] of the [`Overlay`] + /// * the current cursor position + /// * a mutable `Message` list, allowing the [`Overlay`] to produce + /// new messages based on user interaction. + /// * the `Renderer` + /// * a [`Clipboard`], if available + /// + /// By default, it does nothing. + /// + /// [`Event`]: ../enum.Event.html + /// [`Overlay`]: trait.Widget.html + /// [`Layout`]: ../layout/struct.Layout.html + /// [`Clipboard`]: ../trait.Clipboard.html + fn on_event( + &mut self, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + } +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs new file mode 100644 index 00000000..e1fd9b88 --- /dev/null +++ b/native/src/overlay/element.rs @@ -0,0 +1,170 @@ +pub use crate::Overlay; + +use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size, Vector}; + +/// A generic [`Overlay`]. +/// +/// [`Overlay`]: trait.Overlay.html +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Creates a new [`Element`] containing the given [`Overlay`]. + /// + /// [`Element`]: struct.Element.html + /// [`Overlay`]: trait.Overlay.html + pub fn new( + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, + ) -> Self { + Self { position, overlay } + } + + /// Translates the [`Element`]. + /// + /// [`Element`]: struct.Element.html + pub fn translate(mut self, translation: Vector) -> Self { + self.position = self.position + translation; + self + } + + /// Applies a transformation to the produced message of the [`Element`]. + /// + /// [`Element`]: struct.Element.html + pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: 'a, + B: 'static, + { + Element { + position: self.position, + overlay: Box::new(Map::new(self.overlay, f)), + } + } + + /// Computes the layout of the [`Element`] in the given bounds. + /// + /// [`Element`]: struct.Element.html + pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.overlay.layout(renderer, bounds, self.position) + } + + /// Processes a runtime [`Event`]. + /// + /// [`Event`]: enum.Event.html + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) { + self.overlay.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + /// Draws the [`Element`] and its children using the given [`Layout`]. + /// + /// [`Element`]: struct.Element.html + /// [`Layout`]: layout/struct.Layout.html + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.overlay + .draw(renderer, defaults, layout, cursor_position) + } + + /// Computes the _layout_ hash of the [`Element`]. + /// + /// [`Element`]: struct.Element.html + pub fn hash_layout(&self, state: &mut Hasher) { + self.overlay.hash_layout(state, self.position); + } +} + +struct Map<'a, A, B, Renderer> { + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.content.layout(renderer, bounds, position) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<B>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) { + let mut original_messages = Vec::new(); + + self.content.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.content + .draw(renderer, defaults, layout, cursor_position) + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + self.content.hash_layout(state, position); + } +} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs new file mode 100644 index 00000000..0d4bc63c --- /dev/null +++ b/native/src/overlay/menu.rs @@ -0,0 +1,464 @@ +//! Build and show dropdown menus. +use crate::{ + container, layout, mouse, overlay, scrollable, text, Clipboard, Container, + Element, Event, Hasher, Layout, Length, Point, Rectangle, Scrollable, Size, + Vector, Widget, +}; + +/// A list of selectable options. +#[allow(missing_debug_implementations)] +pub struct Menu<'a, T, Message, Renderer: self::Renderer> { + state: &'a mut State, + options: &'a [T], + on_selected: &'a dyn Fn(T) -> Message, + width: u16, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: self::Renderer + 'a, +{ + /// Creates a new [`Menu`] with the given [`State`], a list of options, and + /// the message to produced when an option is selected. + /// + /// [`Menu`]: struct.Menu.html + /// [`State`]: struct.State.html + pub fn new( + state: &'a mut State, + options: &'a [T], + on_selected: &'a dyn Fn(T) -> Message, + ) -> Self { + Menu { + state, + options, + on_selected, + width: 0, + padding: 0, + text_size: None, + font: Default::default(), + style: Default::default(), + } + } + + /// Sets the width of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn width(mut self, width: u16) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the font of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Turns the [`Menu`] into an overlay [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + pub fn overlay( + self, + position: Point, + target_height: f32, + ) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new( + position, + Box::new(Overlay::new(self, target_height)), + ) + } +} + +/// The local state of a [`Menu`]. +/// +/// [`Menu`]: struct.Menu.html +#[derive(Debug, Clone, Default)] +pub struct State { + scrollable: scrollable::State, + hovered_option: Option<usize>, + is_open: bool, +} + +impl State { + /// Returns whether the [`Menu`] is currently open or not. + /// + /// [`Menu`]: struct.Menu.html + pub fn is_open(&self) -> bool { + self.is_open + } + + /// Opens the [`Menu`] with the given option hovered by default. + /// + /// [`Menu`]: struct.Menu.html + pub fn open(&mut self, hovered_option: Option<usize>) { + self.is_open = true; + self.hovered_option = hovered_option; + } +} + +struct Overlay<'a, Message, Renderer: self::Renderer> { + container: Container<'a, Message, Renderer>, + is_open: &'a mut bool, + width: u16, + target_height: f32, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a, +{ + pub fn new<T>( + menu: Menu<'a, T, Message, Renderer>, + target_height: f32, + ) -> Self + where + T: Clone + ToString, + { + let Menu { + state, + options, + on_selected, + width, + padding, + font, + text_size, + style, + } = menu; + + let container = + Container::new(Scrollable::new(&mut state.scrollable).push(List { + options, + hovered_option: &mut state.hovered_option, + on_selected, + font, + text_size, + padding, + style: style.clone(), + })) + .padding(1); + + Self { + container, + is_open: &mut state.is_open, + width: width, + target_height, + style: style, + } + } +} + +impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> + for Overlay<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let space_below = bounds.height - (position.y + self.target_height); + let space_above = position.y; + + let limits = layout::Limits::new( + Size::ZERO, + Size::new( + bounds.width - position.x, + if space_below > space_above { + space_below + } else { + space_above + }, + ), + ) + .width(Length::Units(self.width)); + + let mut node = self.container.layout(renderer, &limits); + + node.move_to(if space_below > space_above { + position + Vector::new(0.0, self.target_height) + } else { + position - Vector::new(0.0, node.size().height) + }); + + node + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + use std::hash::Hash; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + (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<Message>, + 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, + ); + + let option_was_selected = current_messages < messages.len(); + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + if !bounds.contains(cursor_position) || option_was_selected => + { + *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, + &self.style, + primitives, + ) + } +} + +struct List<'a, T, Message, Renderer: self::Renderer> { + options: &'a [T], + hovered_option: &'a mut Option<usize>, + on_selected: &'a dyn Fn(T) -> Message, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer> + for List<'a, T, Message, Renderer> +where + T: Clone + ToString, + 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 text_size = self.text_size.unwrap_or(renderer.default_size()); + + let size = { + let intrinsic = Size::new( + 0.0, + f32::from(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 _; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.options.len().hash(state); + self.text_size.hash(state); + self.padding.hash(state); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + 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(); + let text_size = + self.text_size.unwrap_or(renderer.default_size()); + + if bounds.contains(cursor_position) { + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / f32::from(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.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } +} + +/// The renderer of a [`Menu`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Menu`] in your user interface. +/// +/// [`Menu`]: struct.Menu.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: + scrollable::Renderer + container::Renderer + text::Renderer +{ + /// The [`Menu`] style supported by this renderer. + /// + /// [`Menu`]: struct.Menu.html + type Style: Default + Clone; + + /// Decorates a the list of options of a [`Menu`]. + /// + /// This method can be used to draw a background for the [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + fn decorate( + &mut self, + bounds: Rectangle, + cursor_position: Point, + style: &<Self as Renderer>::Style, + primitive: Self::Output, + ) -> Self::Output; + + /// Draws the list of options of a [`Menu`]. + /// + /// [`Menu`]: struct.Menu.html + fn draw<T: ToString>( + &mut self, + bounds: Rectangle, + cursor_position: Point, + options: &[T], + hovered_option: Option<usize>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> + for List<'a, T, Message, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: 'a + self::Renderer, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/program/state.rs b/native/src/program/state.rs index fdc42e8b..95e6b60c 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -35,7 +35,7 @@ where renderer: &mut P::Renderer, debug: &mut Debug, ) -> Self { - let user_interface = build_user_interface( + let mut user_interface = build_user_interface( &mut program, Cache::default(), renderer, @@ -121,12 +121,14 @@ where debug.event_processing_started(); let mut messages = user_interface.update( - self.queued_events.drain(..), + &self.queued_events, cursor_position, clipboard, renderer, ); messages.extend(self.queued_messages.drain(..)); + + self.queued_events.clear(); debug.event_processing_finished(); if messages.is_empty() { @@ -153,7 +155,7 @@ where command })); - let user_interface = build_user_interface( + let mut user_interface = build_user_interface( &mut self.program, temp_cache, renderer, diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a16df72b..d986141f 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -25,7 +25,7 @@ mod null; #[cfg(debug_assertions)] pub use null::Null; -use crate::{layout, Element}; +use crate::{layout, Element, Rectangle}; /// A component that can take the state of a user interface and produce an /// output for its users. @@ -56,4 +56,13 @@ pub trait Renderer: Sized { ) -> layout::Node { element.layout(self, limits) } + + /// Overlays the `overlay` output with the given bounds on top of the `base` + /// output. + fn overlay( + &mut self, + base: Self::Output, + overlay: Self::Output, + overlay_bounds: Rectangle, + ) -> Self::Output; } diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 580f58f8..07f79319 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -23,6 +23,9 @@ impl Null { impl Renderer for Null { type Output = (); type Defaults = (); + + fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) { + } } impl column::Renderer for Null { diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index b9646043..00a290f1 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,4 @@ -use crate::{layout, Clipboard, Element, Event, Layout, Point, Size}; +use crate::{layout, overlay, Clipboard, Element, Event, Layout, Point, Size}; use std::hash::Hasher; @@ -19,9 +19,9 @@ use std::hash::Hasher; /// [`UserInterface`]: struct.UserInterface.html #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { - hash: u64, root: Element<'a, Message, Renderer>, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, } @@ -94,25 +94,36 @@ where ) -> Self { let root = root.into(); - let hash = { - let hasher = &mut crate::Hasher::default(); - root.hash_layout(hasher); + let (base, overlay) = { + let hash = { + let hasher = &mut crate::Hasher::default(); + root.hash_layout(hasher); - hasher.finish() - }; + hasher.finish() + }; - let layout_is_cached = hash == cache.hash && bounds == cache.bounds; + let layout_is_cached = + hash == cache.base.hash && bounds == cache.bounds; - let layout = if layout_is_cached { - cache.layout - } else { - renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) + let (layout, overlay) = if layout_is_cached { + (cache.base.layout, cache.overlay) + } else { + ( + renderer.layout( + &root, + &layout::Limits::new(Size::ZERO, bounds), + ), + None, + ) + }; + + (Layer { layout, hash }, overlay) }; UserInterface { - hash, root, - layout, + base, + overlay, bounds, } } @@ -169,7 +180,7 @@ where /// /// // Update the user interface /// let messages = user_interface.update( - /// events.drain(..), + /// &events, /// cursor_position, /// None, /// &renderer, @@ -185,18 +196,54 @@ where /// ``` pub fn update( &mut self, - events: impl IntoIterator<Item = Event>, + events: &[Event], cursor_position: Point, clipboard: Option<&dyn Clipboard>, renderer: &Renderer, ) -> Vec<Message> { let mut messages = Vec::new(); + let base_cursor = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, + renderer, + ); + + for event in events { + overlay.on_event( + event.clone(), + Layout::new(&layer.layout), + cursor_position, + &mut messages, + renderer, + clipboard, + ); + } + + let base_cursor = if layer.layout.bounds().contains(cursor_position) + { + // TODO: Type-safe cursor availability + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + self.overlay = Some(layer); + + base_cursor + } else { + cursor_position + }; + for event in events { self.root.widget.on_event( - event, - Layout::new(&self.layout), - cursor_position, + event.clone(), + Layout::new(&self.base.layout), + base_cursor, &mut messages, renderer, clipboard, @@ -256,7 +303,7 @@ where /// ); /// /// let messages = user_interface.update( - /// events.drain(..), + /// &events, /// cursor_position, /// None, /// &renderer, @@ -276,16 +323,63 @@ where /// } /// ``` pub fn draw( - &self, + &mut self, renderer: &mut Renderer, cursor_position: Point, ) -> Renderer::Output { - self.root.widget.draw( - renderer, - &Renderer::Defaults::default(), - Layout::new(&self.layout), - cursor_position, - ) + let overlay = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, + renderer, + ); + + let overlay_bounds = layer.layout.bounds(); + + let overlay_primitives = overlay.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&layer.layout), + cursor_position, + ); + + self.overlay = Some(layer); + + Some((overlay_primitives, overlay_bounds)) + } else { + None + }; + + if let Some((overlay_primitives, overlay_bounds)) = overlay { + let base_cursor = if overlay_bounds.contains(cursor_position) { + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let base_primitives = self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + base_cursor, + ); + + renderer.overlay( + base_primitives, + overlay_primitives, + overlay_bounds, + ) + } else { + self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + cursor_position, + ) + } } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the @@ -295,11 +389,41 @@ where /// [`UserInterface`]: struct.UserInterface.html pub fn into_cache(self) -> Cache { Cache { - hash: self.hash, - layout: self.layout, + base: self.base, + overlay: self.overlay, bounds: self.bounds, } } + + fn overlay_layer( + cache: Option<Layer>, + bounds: Size, + overlay: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + ) -> Layer { + let new_hash = { + let hasher = &mut crate::Hasher::default(); + overlay.hash_layout(hasher); + + hasher.finish() + }; + + let layout = match cache { + Some(Layer { hash, layout }) if new_hash == hash => layout, + _ => overlay.layout(renderer, bounds), + }; + + Layer { + layout, + hash: new_hash, + } + } +} + +#[derive(Debug, Clone)] +struct Layer { + layout: layout::Node, + hash: u64, } /// Reusable data of a specific [`UserInterface`]. @@ -307,8 +431,8 @@ where /// [`UserInterface`]: struct.UserInterface.html #[derive(Debug, Clone)] pub struct Cache { - hash: u64, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, } @@ -322,8 +446,11 @@ impl Cache { /// [`UserInterface`]: struct.UserInterface.html pub fn new() -> Cache { Cache { - hash: 0, - layout: layout::Node::new(Size::new(0.0, 0.0)), + base: Layer { + layout: layout::Node::new(Size::new(0.0, 0.0)), + hash: 0, + }, + overlay: None, bounds: Size::ZERO, } } @@ -334,11 +461,3 @@ impl Default for Cache { Cache::new() } } - -impl PartialEq for Cache { - fn eq(&self, other: &Cache) -> bool { - self.hash == other.hash - } -} - -impl Eq for Cache {} diff --git a/native/src/widget.rs b/native/src/widget.rs index 4453145b..8539e519 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -26,6 +26,7 @@ pub mod column; pub mod container; pub mod image; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod row; @@ -49,6 +50,8 @@ pub use image::Image; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; @@ -67,7 +70,7 @@ pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; -use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::{layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point}; /// A component that displays information and allows interaction. /// @@ -175,4 +178,14 @@ where _clipboard: Option<&dyn Clipboard>, ) { } + + /// Returns the overlay of the [`Element`], if there is any. + /// + /// [`Element`]: struct.Element.html + fn overlay( + &mut self, + _layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + None + } } diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 259a7e6e..42cfe9b9 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -2,8 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, + Point, Widget, }; use std::u32; @@ -204,6 +204,17 @@ where child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Column`]. diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 2590fe3b..b8316e62 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -2,8 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, + Point, Rectangle, Widget, }; use std::u32; @@ -214,6 +214,13 @@ where self.content.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.content.overlay(layout.children().next().unwrap()) + } } /// The renderer of a [`Container`]. diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index c472d043..0b237b9e 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -29,8 +29,8 @@ pub use state::{Focus, State}; pub use title_bar::TitleBar; use crate::{ - container, keyboard, layout, mouse, row, text, Clipboard, Element, Event, - Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, + container, keyboard, layout, mouse, overlay, row, text, Clipboard, Element, + Event, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -636,6 +636,17 @@ where element.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter_mut() + .zip(layout.children()) + .filter_map(|((_, pane), layout)| pane.overlay(layout)) + .next() + } } /// The renderer of a [`PaneGrid`]. diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 1f5ce640..39a92186 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,5 +1,6 @@ use crate::container; use crate::layout; +use crate::overlay; use crate::pane_grid::{self, TitleBar}; use crate::{Clipboard, Element, Event, Hasher, Layout, Point, Size}; @@ -184,6 +185,24 @@ where pub(crate) fn hash_layout(&self, state: &mut Hasher) { self.body.hash_layout(state); } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let body_layout = if self.title_bar.is_some() { + let mut children = layout.children(); + + // Overlays only allowed in the pane body, for now at least. + let _title_bar_layout = children.next(); + + children.next()? + } else { + layout + }; + + self.body.overlay(body_layout) + } } impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs new file mode 100644 index 00000000..9f62e550 --- /dev/null +++ b/native/src/widget/pick_list.rs @@ -0,0 +1,320 @@ +//! Display a dropdown list of selectable values. +use crate::{ + layout, mouse, overlay, + overlay::menu::{self, Menu}, + scrollable, text, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Rectangle, Size, Widget, +}; +use std::borrow::Cow; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: self::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + menu: &'a mut menu::State, + on_selected: Box<dyn Fn(T) -> Message>, + options: Cow<'a, [T]>, + selected: Option<T>, + width: Length, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, + is_open: bool, +} + +/// The local state of a [`PickList`]. +/// +/// [`PickList`]: struct.PickList.html +#[derive(Debug, Clone, Default)] +pub struct State { + menu: menu::State, +} + +impl<'a, T: 'a, Message, Renderer: self::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// 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. + /// + /// [`PickList`]: struct.PickList.html + /// [`State`]: struct.State.html + pub fn new( + state: &'a mut State, + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + let is_open = state.menu.is_open(); + + Self { + menu: &mut state.menu, + on_selected: Box::new(on_selected), + options: options.into(), + selected, + width: Length::Shrink, + text_size: None, + padding: Renderer::DEFAULT_PADDING, + font: Default::default(), + style: Default::default(), + is_open, + } + } + + /// Sets the width of the [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +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: 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(self.padding), + 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<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + if !self.is_open { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => { + if layout.bounds().contains(cursor_position) { + let selected = self.selected.as_ref(); + + self.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.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + if self.menu.is_open() { + let bounds = layout.bounds(); + + let mut menu = + Menu::new(&mut self.menu, &self.options, &self.on_selected) + .width(bounds.width.round() as u16) + .padding(self.padding) + .font(self.font) + .style(Renderer::menu_style(&self.style)); + + if let Some(text_size) = self.text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } + } +} + +/// The renderer of a [`PickList`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PickList`] in your user interface. +/// +/// [`PickList`]: struct.PickList.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: text::Renderer + menu::Renderer { + /// The default padding of a [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + const DEFAULT_PADDING: u16; + + /// The [`PickList`] style supported by this renderer. + /// + /// [`PickList`]: struct.PickList.html + type Style: Default; + + /// Returns the style of the [`Menu`] of the [`PickList`]. + /// + /// [`Menu`]: ../../overlay/menu/struct.Menu.html + /// [`PickList`]: struct.PickList.html + fn menu_style( + style: &<Self as Renderer>::Style, + ) -> <Self as menu::Renderer>::Style; + + /// Draws a [`PickList`]. + /// + /// [`PickList`]: struct.PickList.html + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: self::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 31f7472f..2b6db224 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -2,8 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, + Point, Widget, }; use std::u32; @@ -206,6 +206,17 @@ where child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Row`]. diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 3c8e5e5b..75e97027 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. use crate::{ - column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, - Layout, Length, Point, Rectangle, Size, Widget, + column, layout, mouse, overlay, Align, Clipboard, Column, Element, Event, + Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -113,7 +113,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer + column::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { Widget::<Message, Renderer>::width(&self.content) @@ -315,6 +315,24 @@ where self.content.hash_layout(state) } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let Self { content, state, .. } = self; + + content + .overlay(layout.children().next().unwrap()) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } } /// The local state of a [`Scrollable`]. @@ -454,7 +472,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 +520,7 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + column::Renderer, + Renderer: 'a + self::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index e56a8fe1..f1576ffb 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -43,7 +43,7 @@ impl Space { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: self::Renderer, { diff --git a/src/widget.rs b/src/widget.rs index 007bd531..b26f14d4 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, container, pane_grid, pick_list, progress_bar, radio, scrollable, slider, text_input, Column, Row, Space, Text, }; @@ -45,8 +45,8 @@ 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, + pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, + radio::Radio, scrollable::Scrollable, slider::Slider, svg::Svg, text_input::TextInput, }; diff --git a/style/src/lib.rs b/style/src/lib.rs index 72d83aec..8e402bb1 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -7,6 +7,8 @@ pub use iced_core::{Background, Color}; pub mod button; pub mod checkbox; pub mod container; +pub mod menu; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod scrollable; diff --git a/style/src/menu.rs b/style/src/menu.rs new file mode 100644 index 00000000..e8321dc7 --- /dev/null +++ b/style/src/menu.rs @@ -0,0 +1,25 @@ +use iced_core::{Background, Color}; + +/// The appearance of a menu. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub text_color: Color, + pub background: Background, + pub border_width: u16, + pub border_color: Color, + pub selected_text_color: Color, + pub selected_background: Background, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + text_color: Color::BLACK, + background: Background::Color([0.87, 0.87, 0.87].into()), + border_width: 1, + border_color: [0.7, 0.7, 0.7].into(), + selected_text_color: Color::WHITE, + selected_background: Background::Color([0.4, 0.4, 1.0].into()), + } + } +} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs new file mode 100644 index 00000000..fbd431c0 --- /dev/null +++ b/style/src/pick_list.rs @@ -0,0 +1,70 @@ +use crate::menu; +use iced_core::{Background, Color}; + +/// The appearance of a pick list. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub text_color: Color, + pub background: Background, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, + pub icon_size: f32, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + text_color: Color::BLACK, + background: Background::Color([0.87, 0.87, 0.87].into()), + border_radius: 0, + border_width: 1, + border_color: [0.7, 0.7, 0.7].into(), + icon_size: 0.7, + } + } +} + +/// A set of rules that dictate the style of a container. +pub trait StyleSheet { + fn menu(&self) -> menu::Style; + + fn active(&self) -> Style; + + /// Produces the style of a container. + fn hovered(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn menu(&self) -> menu::Style { + menu::Style::default() + } + + fn active(&self) -> Style { + Style::default() + } + + fn hovered(&self) -> Style { + Style { + border_color: Color::BLACK, + ..self.active() + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index a25f42f7..c71a6a77 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -248,6 +248,7 @@ impl iced_graphics::Backend for Backend { impl backend::Text for Backend { const ICON_FONT: Font = font::ICONS; const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; fn default_size(&self) -> u16 { self.default_text_size diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index d17b7a5d..ced64332 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -13,6 +13,7 @@ pub mod button; pub mod checkbox; pub mod container; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod scrollable; @@ -28,6 +29,8 @@ pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; diff --git a/wgpu/src/widget/pick_list.rs b/wgpu/src/widget/pick_list.rs new file mode 100644 index 00000000..fccc68c9 --- /dev/null +++ b/wgpu/src/widget/pick_list.rs @@ -0,0 +1,9 @@ +//! Display a dropdown list of selectable values. +pub use iced_native::pick_list::State; + +pub use iced_graphics::overlay::menu::Style as Menu; +pub use iced_graphics::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message> = + iced_native::PickList<'a, T, Message, crate::Renderer>; |