diff options
author | 2022-03-23 17:11:14 +0700 | |
---|---|---|
committer | 2022-03-23 17:11:14 +0700 | |
commit | 0eef527fa5b04be74141c75b076677473320e321 (patch) | |
tree | 5062a9ce2c370632de87a01471526da1176e0a60 | |
parent | 4aece6b77617f4a37af8208d8ddb1618bf9052d3 (diff) | |
parent | ef4c79ea23e86fec9a8ad0fb27463296c14400e5 (diff) | |
download | iced-0eef527fa5b04be74141c75b076677473320e321.tar.gz iced-0eef527fa5b04be74141c75b076677473320e321.tar.bz2 iced-0eef527fa5b04be74141c75b076677473320e321.zip |
Merge pull request #1284 from iced-rs/virtual-widgets
Stateless widgets
140 files changed, 12597 insertions, 2573 deletions
@@ -14,24 +14,20 @@ resolver = "2" [features] default = ["wgpu"] -# Enables the `iced_wgpu` renderer -wgpu = ["iced_wgpu"] # Enables the `Image` widget image = ["iced_wgpu/image"] # Enables the `Svg` widget svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget -canvas = ["iced_wgpu/canvas"] +canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget -qr_code = ["iced_wgpu/qr_code"] +qr_code = ["iced_graphics/qr_code"] +# Enables the `iced_wgpu` renderer +wgpu = ["iced_wgpu"] # Enables using system fonts default_system_font = ["iced_wgpu/default_system_font"] # Enables the `iced_glow` renderer. Overrides `iced_wgpu` glow = ["iced_glow", "iced_glutin"] -# Enables the `Canvas` widget for `iced_glow` -glow_canvas = ["iced_glow/canvas"] -# Enables the `QRCode` widget for `iced_glow` -glow_qr_code = ["iced_glow/qr_code"] # Enables using system fonts for `iced_glow` glow_default_system_font = ["iced_glow/default_system_font"] # Enables a debug view in native platforms (press F12) @@ -44,6 +40,8 @@ async-std = ["iced_futures/async-std"] smol = ["iced_futures/smol"] # Enables advanced color conversion via `palette` palette = ["iced_core/palette"] +# Enables pure, virtual widgets in the `pure` module +pure = ["iced_pure", "iced_graphics/pure"] [badges] maintenance = { status = "actively-developed" } @@ -57,6 +55,7 @@ members = [ "glutin", "lazy", "native", + "pure", "style", "wgpu", "winit", @@ -87,15 +86,25 @@ members = [ "examples/tooltip", "examples/tour", "examples/url_handler", + "examples/pure/component", + "examples/pure/counter", + "examples/pure/game_of_life", + "examples/pure/pane_grid", + "examples/pure/pick_list", + "examples/pure/todos", + "examples/pure/tour", "examples/websocket", ] [dependencies] iced_core = { version = "0.4", path = "core" } iced_futures = { version = "0.3", path = "futures" } +iced_native = { version = "0.4", path = "native" } +iced_graphics = { version = "0.2", path = "graphics" } iced_winit = { version = "0.3", path = "winit" } iced_glutin = { version = "0.2", path = "glutin", optional = true } iced_glow = { version = "0.2", path = "glow", optional = true } +iced_pure = { version = "0.1", path = "pure", optional = true } thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 325ccc1a..3b8a1d6a 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -76,7 +76,7 @@ impl Application for Clock { } } -impl canvas::Program<Message> for Clock { +impl<Message> canvas::Program<Message> for Clock { fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { let clock = self.clock.draw(bounds.size(), |frame| { let center = frame.center(); diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 682dc65b..f5fab251 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -235,7 +235,7 @@ impl Theme { } } -impl canvas::Program<Message> for Theme { +impl<Message> canvas::Program<Message> for Theme { fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { let theme = self.canvas_cache.draw(bounds.size(), |frame| { self.draw(frame); diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml new file mode 100644 index 00000000..b6c7a513 --- /dev/null +++ b/examples/pure/component/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pure_component" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } +iced_native = { path = "../../../native" } +iced_lazy = { path = "../../../lazy", features = ["pure"] } +iced_pure = { path = "../../../pure" } diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs new file mode 100644 index 00000000..b38d6fca --- /dev/null +++ b/examples/pure/component/src/main.rs @@ -0,0 +1,166 @@ +use iced::pure::container; +use iced::pure::{Element, Sandbox}; +use iced::{Length, Settings}; + +use numeric_input::numeric_input; + +pub fn main() -> iced::Result { + Component::run(Settings::default()) +} + +#[derive(Default)] +struct Component { + value: Option<u32>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + NumericInputChanged(Option<u32>), +} + +impl Sandbox for Component { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Component - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::NumericInputChanged(value) => { + self.value = value; + } + } + } + + fn view(&self) -> Element<Message> { + container(numeric_input(self.value, Message::NumericInputChanged)) + .padding(20) + .height(Length::Fill) + .center_y() + .into() + } +} + +mod numeric_input { + use iced::pure::{button, row, text, text_input}; + use iced_lazy::pure::{self, Component}; + use iced_native::alignment::{self, Alignment}; + use iced_native::text; + use iced_native::Length; + use iced_pure::Element; + + pub struct NumericInput<Message> { + value: Option<u32>, + on_change: Box<dyn Fn(Option<u32>) -> Message>, + } + + pub fn numeric_input<Message>( + value: Option<u32>, + on_change: impl Fn(Option<u32>) -> Message + 'static, + ) -> NumericInput<Message> { + NumericInput::new(value, on_change) + } + + #[derive(Debug, Clone)] + pub enum Event { + InputChanged(String), + IncrementPressed, + DecrementPressed, + } + + impl<Message> NumericInput<Message> { + pub fn new( + value: Option<u32>, + on_change: impl Fn(Option<u32>) -> Message + 'static, + ) -> Self { + Self { + value, + on_change: Box::new(on_change), + } + } + } + + impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message> + where + Renderer: text::Renderer + 'static, + { + type State = (); + type Event = Event; + + fn update( + &mut self, + _state: &mut Self::State, + event: Event, + ) -> Option<Message> { + match event { + Event::IncrementPressed => Some((self.on_change)(Some( + self.value.unwrap_or_default().saturating_add(1), + ))), + Event::DecrementPressed => Some((self.on_change)(Some( + self.value.unwrap_or_default().saturating_sub(1), + ))), + Event::InputChanged(value) => { + if value.is_empty() { + Some((self.on_change)(None)) + } else { + value + .parse() + .ok() + .map(Some) + .map(self.on_change.as_ref()) + } + } + } + } + + fn view(&self, _state: &Self::State) -> Element<Event, Renderer> { + let button = |label, on_press| { + button( + text(label) + .width(Length::Fill) + .height(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .vertical_alignment(alignment::Vertical::Center), + ) + .width(Length::Units(50)) + .on_press(on_press) + }; + + row() + .push(button("-", Event::DecrementPressed)) + .push( + text_input( + "Type a number", + self.value + .as_ref() + .map(u32::to_string) + .as_ref() + .map(String::as_str) + .unwrap_or(""), + Event::InputChanged, + ) + .padding(10), + ) + .push(button("+", Event::IncrementPressed)) + .align_items(Alignment::Fill) + .spacing(10) + .into() + } + } + + impl<'a, Message, Renderer> From<NumericInput<Message>> + for Element<'a, Message, Renderer> + where + Message: 'a, + Renderer: 'static + text::Renderer, + { + fn from(numeric_input: NumericInput<Message>) -> Self { + pure::component(numeric_input) + } + } +} diff --git a/examples/pure/counter/Cargo.toml b/examples/pure/counter/Cargo.toml new file mode 100644 index 00000000..2fcd22d4 --- /dev/null +++ b/examples/pure/counter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pure_counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs new file mode 100644 index 00000000..726009df --- /dev/null +++ b/examples/pure/counter/src/main.rs @@ -0,0 +1,49 @@ +use iced::pure::{button, column, text, Element, Sandbox}; +use iced::{Alignment, Settings}; + +pub fn main() -> iced::Result { + Counter::run(Settings::default()) +} + +struct Counter { + value: i32, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + IncrementPressed, + DecrementPressed, +} + +impl Sandbox for Counter { + type Message = Message; + + fn new() -> Self { + Self { value: 0 } + } + + fn title(&self) -> String { + String::from("Counter - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::IncrementPressed => { + self.value += 1; + } + Message::DecrementPressed => { + self.value -= 1; + } + } + } + + fn view(&self) -> Element<Message> { + column() + .padding(20) + .align_items(Alignment::Center) + .push(button("Increment").on_press(Message::IncrementPressed)) + .push(text(self.value.to_string()).size(50)) + .push(button("Decrement").on_press(Message::DecrementPressed)) + .into() + } +} diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml new file mode 100644 index 00000000..22e38f00 --- /dev/null +++ b/examples/pure/game_of_life/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pure_game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure", "canvas", "tokio", "debug"] } +tokio = { version = "1.0", features = ["sync"] } +itertools = "0.9" +rustc-hash = "1.1" +env_logger = "0.9" diff --git a/examples/pure/game_of_life/README.md b/examples/pure/game_of_life/README.md new file mode 100644 index 00000000..aa39201c --- /dev/null +++ b/examples/pure/game_of_life/README.md @@ -0,0 +1,22 @@ +## Game of Life + +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The __[`main`]__ file contains the relevant code of the example. + +<div align="center"> + <a href="https://gfycat.com/WhichPaltryChick"> + <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[`main`]: src/main.rs +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/pure/game_of_life/src/main.rs b/examples/pure/game_of_life/src/main.rs new file mode 100644 index 00000000..a3164701 --- /dev/null +++ b/examples/pure/game_of_life/src/main.rs @@ -0,0 +1,898 @@ +//! 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::executor; +use iced::pure::{ + button, checkbox, column, container, pick_list, row, slider, text, +}; +use iced::pure::{Application, Element}; +use iced::time; +use iced::window; +use iced::{Alignment, Color, Command, Length, Settings, Subscription}; +use preset::Preset; +use std::time::{Duration, Instant}; + +pub fn main() -> iced::Result { + env_logger::builder().format_timestamp(None).init(); + + GameOfLife::run(Settings { + antialiasing: true, + window: window::Settings { + position: window::Position::Centered, + ..window::Settings::default() + }, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + is_playing: bool, + queued_ticks: usize, + speed: usize, + next_speed: Option<usize>, + version: usize, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message, usize), + Tick(Instant), + TogglePlayback, + ToggleGrid(bool), + Next, + Clear, + SpeedChanged(f32), + PresetPicked(Preset), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self { + speed: 5, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn background_color(&self) -> Color { + style::BACKGROUND + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Grid(message, version) => { + if version == self.version { + self.grid.update(message); + } + } + Message::Tick(_) | Message::Next => { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + + if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + self.queued_ticks = 0; + + let version = self.version; + + return Command::perform(task, move |message| { + Message::Grid(message, version) + }); + } + } + Message::TogglePlayback => { + self.is_playing = !self.is_playing; + } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } + Message::Clear => { + self.grid.clear(); + self.version += 1; + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; + } + } + Message::PresetPicked(new_preset) => { + self.grid = Grid::from_preset(new_preset); + self.version += 1; + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&self) -> Element<Message> { + let version = self.version; + let selected_speed = self.next_speed.unwrap_or(self.speed); + let controls = view_controls( + self.is_playing, + self.grid.are_lines_visible(), + selected_speed, + self.grid.preset(), + ); + + let content = column() + .push( + self.grid + .view() + .map(move |message| Message::Grid(message, version)), + ) + .push(controls); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +fn view_controls<'a>( + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + preset: Preset, +) -> Element<'a, Message> { + let playback_controls = row() + .spacing(10) + .push( + button(if is_playing { "Pause" } else { "Play" }) + .on_press(Message::TogglePlayback) + .style(style::Button), + ) + .push(button("Next").on_press(Message::Next).style(style::Button)); + + let speed_controls = row() + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10) + .push( + slider(1.0..=1000.0, speed as f32, Message::SpeedChanged) + .style(style::Slider), + ) + .push(text(format!("x{}", speed)).size(16)); + + row() + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .push(playback_controls) + .push(speed_controls) + .push( + checkbox("Grid", is_grid_enabled, Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + ) + .push( + pick_list(preset::ALL, Some(preset), Message::PresetPicked) + .padding(8) + .text_size(16) + .style(style::PickList), + ) + .push(button("Clear").on_press(Message::Clear).style(style::Clear)) + .into() +} + +mod grid { + use crate::Preset; + use iced::pure::widget::canvas::event::{self, Event}; + use iced::pure::widget::canvas::{ + self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text, + }; + use iced::pure::Element; + use iced::{ + alignment, mouse, Color, Length, Point, Rectangle, Size, Vector, + }; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; + use std::ops::RangeInclusive; + use std::time::{Duration, Instant}; + + pub struct Grid { + state: State, + preset: Preset, + life_cache: Cache, + grid_cache: Cache, + translation: Vector, + scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, + } + + #[derive(Debug, Clone)] + pub enum Message { + Populate(Cell), + Unpopulate(Cell), + Translated(Vector), + Scaled(f32, Option<Vector>), + Ticked { + result: Result<Life, TickError>, + tick_duration: Duration, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, + } + + 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::with_life( + preset + .life() + .into_iter() + .map(|(i, j)| Cell { i, j }) + .collect(), + ), + preset, + life_cache: Cache::default(), + grid_cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, + } + } + + pub fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Message>> { + let tick = self.state.tick(amount)?; + + self.last_queued_ticks = amount; + + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + tick_duration, + } + }) + } + + pub fn update(&mut self, message: Message) { + match message { + 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::Translated(translation) => { + self.translation = translation; + + self.life_cache.clear(); + self.grid_cache.clear(); + } + Message::Scaled(scaling, translation) => { + self.scaling = scaling; + + if let Some(translation) = translation { + self.translation = translation; + } + + self.life_cache.clear(); + self.grid_cache.clear(); + } + Message::Ticked { + result: Ok(life), + tick_duration, + } => { + self.state.update(life); + self.life_cache.clear(); + + self.last_tick_duration = tick_duration; + } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + } + } + + pub fn view<'a>(&'a self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.preset = Preset::Custom; + + self.life_cache.clear(); + } + + pub fn preset(&self) -> Preset { + self.preset + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Region { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } + } + + impl canvas::Program<Message> for Grid { + type State = Interaction; + + fn update( + &self, + interaction: &mut Interaction, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option<Message>) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + *interaction = Interaction::None; + } + + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; + + let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); + + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) + } else { + (Some(Message::Populate(cell)), None) + }; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(button) => { + let message = match button { + mouse::Button::Left => { + *interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + *interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; + + None + } + _ => None, + }; + + (event::Status::Captured, message) + } + mouse::Event::CursorMoved { .. } => { + let message = match *interaction { + Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, + Interaction::Panning { translation, start } => { + Some(Message::Translated( + translation + + (cursor_position - start) + * (1.0 / self.scaling), + )) + } + _ => None, + }; + + let event_status = match interaction { + Interaction::None => event::Status::Ignored, + _ => event::Status::Captured, + }; + + (event_status, message) + } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING + { + let old_scaling = self.scaling; + + let scaling = (self.scaling * (1.0 + y / 30.0)) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); + + let translation = + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + let factor = scaling - old_scaling; + + Some( + self.translation + - Vector::new( + cursor_to_center.x * factor + / (old_scaling + * old_scaling), + cursor_to_center.y * factor + / (old_scaling + * old_scaling), + ), + ) + } else { + None + }; + + ( + event::Status::Captured, + Some(Message::Scaled(scaling, translation)), + ) + } else { + (event::Status::Captured, None) + } + } + }, + _ => (event::Status::Ignored, None), + }, + _ => (event::Status::Ignored, None), + } + } + + fn draw( + &self, + _interaction: &Interaction, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); + + let life = self.life_cache.draw(bounds.size(), |frame| { + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + + for cell in region.cull(self.state.cells()) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.size()); + + let hovered_cell = + cursor.position_in(&bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); + + if let Some(cell) = hovered_cell { + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); + } + + let cell_count = self.state.cell_count(); + + frame.fill_text(Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + + frame.into_geometry() + }; + + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, overlay] + } + } + + fn mouse_interaction( + &self, + interaction: &Interaction, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + match interaction { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), + } + } + } + + #[derive(Default)] + struct State { + life: Life, + births: FxHashSet<Cell>, + is_ticking: bool, + } + + impl State { + pub fn with_life(life: Life) -> Self { + Self { + life, + ..Self::default() + } + } + + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator<Item = &Cell> { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Result<Life, TickError>>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } + } + + #[derive(Clone, Default)] + pub struct Life { + cells: FxHashSet<Cell>, + } + + impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator<Item = &Cell> { + self.cells.iter() + } + } + + 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") + .field("cells", &self.cells.len()) + .finish() + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator<Item = Cell> { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + } + + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn rows(&self) -> RangeInclusive<isize> { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive<isize> { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator<Item = &'a Cell>, + ) -> impl Iterator<Item = &'a Cell> { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + + pub enum Interaction { + None, + Drawing, + Erasing, + Panning { translation: Vector, start: Point }, + } + + impl Default for Interaction { + fn default() -> Self { + Self::None + } + } +} diff --git a/examples/pure/game_of_life/src/preset.rs b/examples/pure/game_of_life/src/preset.rs new file mode 100644 index 00000000..05157b6a --- /dev/null +++ b/examples/pure/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/pure/game_of_life/src/style.rs b/examples/pure/game_of_life/src/style.rs new file mode 100644 index 00000000..1a64cf4a --- /dev/null +++ b/examples/pure/game_of_life/src/style.rs @@ -0,0 +1,186 @@ +use iced::{button, container, pick_list, slider, Background, Color}; + +const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, +); + +const DESTRUCTIVE: Color = Color::from_rgb( + 0xC0 as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, +); + +pub 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 { + fn style(&self) -> container::Style { + container::Style { + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } +} + +pub struct Button; + +impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3.0, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1.0, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Clear; + +impl button::StyleSheet for Clear { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(DESTRUCTIVE)), + border_radius: 3.0, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color { + a: 0.5, + ..DESTRUCTIVE + })), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1.0, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Slider; + +impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9.0 }, + color: ACTIVE, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } +} + +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.0, + 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.0, + border_color: Color { + a: 0.6, + ..Color::BLACK + }, + border_radius: 2.0, + icon_size: 0.5, + ..pick_list::Style::default() + } + } + + 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/pure/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml new file mode 100644 index 00000000..a51cdaf0 --- /dev/null +++ b/examples/pure/pane_grid/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pure_pane_grid" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["pure", "debug"] } +iced_native = { path = "../../../native" } +iced_lazy = { path = "../../../lazy", features = ["pure"] } diff --git a/examples/pure/pane_grid/src/main.rs b/examples/pure/pane_grid/src/main.rs new file mode 100644 index 00000000..65516956 --- /dev/null +++ b/examples/pure/pane_grid/src/main.rs @@ -0,0 +1,436 @@ +use iced::alignment::{self, Alignment}; +use iced::executor; +use iced::keyboard; +use iced::pure::widget::pane_grid::{self, PaneGrid}; +use iced::pure::{button, column, container, row, scrollable, text}; +use iced::pure::{Application, Element}; +use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced_lazy::pure::responsive; +use iced_native::{event, subscription, Event}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +struct Example { + panes: pane_grid::State<Pane>, + panes_created: usize, + focus: Option<pane_grid::Pane>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Split(pane_grid::Axis, pane_grid::Pane), + SplitFocused(pane_grid::Axis), + FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), + TogglePin(pane_grid::Pane), + Close(pane_grid::Pane), + CloseFocused, +} + +impl Application for Example { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + let (panes, _) = pane_grid::State::new(Pane::new(0)); + + ( + Example { + panes, + panes_created: 1, + focus: None, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Pane grid - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Split(axis, pane) => { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + Message::SplitFocused(axis) => { + if let Some(pane) = self.focus { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + } + Message::FocusAdjacent(direction) => { + if let Some(pane) = self.focus { + if let Some(adjacent) = + self.panes.adjacent(&pane, direction) + { + self.focus = Some(adjacent); + } + } + } + Message::Clicked(pane) => { + self.focus = Some(pane); + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.panes.resize(&split, ratio); + } + Message::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + self.panes.swap(&pane, &target); + } + Message::Dragged(_) => {} + Message::TogglePin(pane) => { + if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane) + { + *is_pinned = !*is_pinned; + } + } + Message::Close(pane) => { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } + } + Message::CloseFocused => { + if let Some(pane) = self.focus { + if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = self.panes.close(&pane) + { + self.focus = Some(sibling); + } + } + } + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.command() => handle_hotkey(key_code), + _ => None, + } + }) + } + + fn view(&self) -> Element<Message> { + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&self.panes, |id, pane| { + let is_focused = focus == Some(id); + + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(Message::TogglePin(id)) + .style(style::Button::Pin) + .padding(3); + + let title = row() + .push(pin_button) + .push("Pane") + .push(text(pane.id.to_string()).color(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + })) + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .controls(view_controls(id, total_panes, pane.is_pinned)) + .padding(10) + .style(if is_focused { + style::TitleBar::Focused + } else { + style::TitleBar::Active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, pane.is_pinned, size) + })) + .title_bar(title_bar) + .style(if is_focused { + style::Pane::Focused + } else { + style::Pane::Active + }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + container(pane_grid) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .into() + } +} + +const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0xC7 as f32 / 255.0, + 0xC7 as f32 / 255.0, +); +const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { + use keyboard::KeyCode; + use pane_grid::{Axis, Direction}; + + let direction = match key_code { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + }; + + match key_code { + KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(Message::CloseFocused), + _ => direction.map(Message::FocusAdjacent), + } +} + +struct Pane { + id: usize, + pub is_pinned: bool, +} + +impl Pane { + fn new(id: usize) -> Self { + Self { + id, + is_pinned: false, + } + } +} + +fn view_content<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, Message> { + let button = |label, message, style| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + .style(style) + }; + + let mut controls = column() + .spacing(5) + .max_width(150) + .push(button( + "Split horizontally", + Message::Split(pane_grid::Axis::Horizontal, pane), + style::Button::Primary, + )) + .push(button( + "Split vertically", + Message::Split(pane_grid::Axis::Vertical, pane), + style::Button::Primary, + )); + + if total_panes > 1 && !is_pinned { + controls = controls.push(button( + "Close", + Message::Close(pane), + style::Button::Destructive, + )); + } + + let content = column() + .width(Length::Fill) + .spacing(10) + .align_items(Alignment::Center) + .push(text(format!("{}x{}", size.width, size.height)).size(24)) + .push(controls); + + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() +} + +fn view_controls<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, +) -> Element<'a, Message> { + let mut button = button(text("Close").size(14)) + .style(style::Button::Control) + .padding(3); + + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); + } + + button.into() +} + +mod style { + use crate::PANE_ID_COLOR_FOCUSED; + use iced::{button, container, Background, Color, Vector}; + + const SURFACE: Color = Color::from_rgb( + 0xF2 as f32 / 255.0, + 0xF3 as f32 / 255.0, + 0xF5 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, + ); + + pub enum TitleBar { + Active, + Focused, + } + + impl container::StyleSheet for TitleBar { + fn style(&self) -> container::Style { + let pane = match self { + Self::Active => Pane::Active, + Self::Focused => Pane::Focused, + } + .style(); + + container::Style { + text_color: Some(Color::WHITE), + background: Some(pane.border_color.into()), + ..Default::default() + } + } + } + + pub enum Pane { + Active, + Focused, + } + + impl container::StyleSheet for Pane { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(SURFACE)), + border_width: 2.0, + border_color: match self { + Self::Active => Color::from_rgb(0.7, 0.7, 0.7), + Self::Focused => Color::BLACK, + }, + ..Default::default() + } + } + } + + pub enum Button { + Primary, + Destructive, + Control, + Pin, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + let (background, text_color) = match self { + Button::Primary => (Some(ACTIVE), Color::WHITE), + Button::Destructive => { + (None, Color::from_rgb8(0xFF, 0x47, 0x47)) + } + Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE), + Button::Pin => (Some(ACTIVE), Color::WHITE), + }; + + button::Style { + text_color, + background: background.map(Background::Color), + border_radius: 5.0, + shadow_offset: Vector::new(0.0, 0.0), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + let background = match self { + Button::Primary => Some(HOVERED), + Button::Destructive => Some(Color { + a: 0.2, + ..active.text_color + }), + Button::Control => Some(PANE_ID_COLOR_FOCUSED), + Button::Pin => Some(HOVERED), + }; + + button::Style { + background: background.map(Background::Color), + ..active + } + } + } +} diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml new file mode 100644 index 00000000..c0fcac3c --- /dev/null +++ b/examples/pure/pick_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pure_pick_list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } diff --git a/examples/pure/pick_list/src/main.rs b/examples/pure/pick_list/src/main.rs new file mode 100644 index 00000000..b9947107 --- /dev/null +++ b/examples/pure/pick_list/src/main.rs @@ -0,0 +1,109 @@ +use iced::pure::{column, container, pick_list, scrollable, vertical_space}; +use iced::pure::{Element, Sandbox}; +use iced::{Alignment, Length, Settings}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + selected_language: Option<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 = Some(language); + } + } + } + + fn view(&self) -> Element<Message> { + let pick_list = pick_list( + &Language::ALL[..], + self.selected_language, + Message::LanguageSelected, + ) + .placeholder("Choose a language..."); + + let content = column() + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10) + .push(vertical_space(Length::Units(600))) + .push("Which is your favorite language?") + .push(pick_list) + .push(vertical_space(Length::Units(600))); + + container(scrollable(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/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml new file mode 100644 index 00000000..217179e8 --- /dev/null +++ b/examples/pure/todos/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pure_todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["async-std", "debug", "default_system_font", "pure"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = "1.0" +directories-next = "2.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3", features = ["Window", "Storage"] } +wasm-timer = "0.2" diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs new file mode 100644 index 00000000..6a6c6300 --- /dev/null +++ b/examples/pure/todos/src/main.rs @@ -0,0 +1,608 @@ +use iced::alignment::{self, Alignment}; +use iced::pure::widget::Text; +use iced::pure::{ + button, checkbox, column, container, row, scrollable, text, text_input, + Application, Element, +}; +use iced::window; +use iced::{Command, Font, Length, Settings}; +use serde::{Deserialize, Serialize}; + +pub fn main() -> iced::Result { + Todos::run(Settings { + window: window::Settings { + size: (500, 800), + ..window::Settings::default() + }, + ..Settings::default() + }) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + input_value: String, + filter: Filter, + tasks: Vec<Task>, + dirty: bool, + saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Result<SavedState, LoadError>), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), +} + +impl Application for Todos { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = (); + + fn new(_flags: ()) -> (Todos, Command<Message>) { + ( + Todos::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) + } + + fn title(&self) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Command<Message> { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + Command::none() + } + Todos::Loaded(state) => { + let mut saved = false; + + match message { + Message::InputChanged(value) => { + state.input_value = value; + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + } + Message::FilterChanged(filter) => { + state.filter = filter; + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + task.update(task_message); + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + } + _ => {} + } + + if !saved { + state.dirty = true; + } + + if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() + } + } + } + } + + fn view(&self) -> Element<Message> { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + input_value, + filter, + tasks, + .. + }) => { + let title = text("todos") + .width(Length::Fill) + .size(100) + .color([0.5, 0.5, 0.5]) + .horizontal_alignment(alignment::Horizontal::Center); + + let input = text_input( + "What needs to be done?", + input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreateTask); + + let controls = view_controls(&tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .fold(column().spacing(20), |column, (i, task)| { + column.push(task.view().map(move |message| { + Message::TaskMessage(i, message) + })) + }) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = column() + .spacing(20) + .max_width(800) + .push(title) + .push(input) + .push(controls) + .push(tasks); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle, + Editing, +} + +impl Default for TaskState { + fn default() -> Self { + Self::Idle + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl Task { + fn new(description: String) -> Self { + Task { + description, + completed: false, + state: TaskState::Idle, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle; + } + } + TaskMessage::Delete => {} + } + } + + fn view(&self) -> Element<TaskMessage> { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( + &self.description, + self.completed, + TaskMessage::Completed, + ) + .width(Length::Fill); + + row() + .spacing(20) + .align_items(Alignment::Center) + .push(checkbox) + .push( + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(style::Button::Icon), + ) + .into() + } + TaskState::Editing => { + let text_input = text_input( + "Describe your task...", + &self.description, + TaskMessage::DescriptionEdited, + ) + .on_submit(TaskMessage::FinishEdition) + .padding(10); + + row() + .spacing(20) + .align_items(Alignment::Center) + .push(text_input) + .push( + button( + row() + .spacing(10) + .push(delete_icon()) + .push("Delete"), + ) + .on_press(TaskMessage::Delete) + .padding(10) + .style(style::Button::Destructive), + ) + .into() + } + } + } +} + +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); + + let button = button(label).style(if filter == current_filter { + style::Button::FilterSelected + } else { + style::Button::FilterActive + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row() + .spacing(20) + .align_items(Alignment::Center) + .push( + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + ) + .push( + row() + .width(Length::Shrink) + .spacing(10) + .push(filter_button("All", Filter::All, current_filter)) + .push(filter_button("Active", Filter::Active, current_filter)) + .push(filter_button( + "Completed", + Filter::Completed, + current_filter, + )), + ) + .into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &Task) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message<'a>() -> Element<'a, Message> { + container( + text("Loading...") + .horizontal_alignment(alignment::Horizontal::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) + .width(Length::Fill) + .size(25) + .horizontal_alignment(alignment::Horizontal::Center) + .color([0.7, 0.7, 0.7]), + ) + .width(Length::Fill) + .height(Length::Units(200)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::External { + name: "Icons", + bytes: include_bytes!("../../../todos/fonts/icons.ttf"), +}; + +fn icon(unicode: char) -> Text { + Text::new(unicode.to_string()) + .font(ICONS) + .width(Length::Units(20)) + .horizontal_alignment(alignment::Horizontal::Center) + .size(20) +} + +fn edit_icon() -> Text { + icon('\u{F303}') +} + +fn delete_icon() -> Text { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec<Task>, +} + +#[derive(Debug, Clone)] +enum LoadError { + FileError, + FormatError, +} + +#[derive(Debug, Clone)] +enum SaveError { + FileError, + WriteError, + FormatError, +} + +#[cfg(not(target_arch = "wasm32"))] +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories_next::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result<SavedState, LoadError> { + use async_std::prelude::*; + + let mut contents = String::new(); + + let mut file = async_std::fs::File::open(Self::path()) + .await + .map_err(|_| LoadError::FileError)?; + + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + use async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::FileError)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::FileError)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::WriteError)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option<web_sys::Storage> { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result<SavedState, LoadError> { + let storage = Self::storage().ok_or(LoadError::FileError)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::FileError)? + .ok_or(LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::FileError)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::WriteError)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + FilterActive, + FilterSelected, + Icon, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + match self { + Button::FilterActive => button::Style::default(), + Button::FilterSelected => button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.2, 0.2, 0.7, + ))), + border_radius: 10.0, + text_color: Color::WHITE, + ..button::Style::default() + }, + Button::Icon => button::Style { + text_color: Color::from_rgb(0.5, 0.5, 0.5), + ..button::Style::default() + }, + Button::Destructive => button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.8, 0.2, 0.2, + ))), + border_radius: 5.0, + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 1.0), + ..button::Style::default() + }, + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + button::Style { + text_color: match self { + Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), + Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7), + _ => active.text_color, + }, + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + } +} diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml new file mode 100644 index 00000000..8ce5f198 --- /dev/null +++ b/examples/pure/tour/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pure_tour" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["image", "debug", "pure"] } +env_logger = "0.8" diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs new file mode 100644 index 00000000..a44d99f3 --- /dev/null +++ b/examples/pure/tour/src/main.rs @@ -0,0 +1,703 @@ +use iced::alignment; +use iced::pure::widget::{Button, Column, Container, Slider}; +use iced::pure::{ + checkbox, column, container, horizontal_space, image, radio, row, + scrollable, slider, text, text_input, toggler, vertical_space, +}; +use iced::pure::{Element, Sandbox}; +use iced::{Color, Length, Settings}; + +pub fn main() -> iced::Result { + env_logger::init(); + + Tour::run(Settings::default()) +} + +pub struct Tour { + steps: Steps, + debug: bool, +} + +impl Sandbox for Tour { + type Message = Message; + + fn new() -> Tour { + Tour { + steps: Steps::new(), + debug: false, + } + } + + fn title(&self) -> String { + format!("{} - Iced", self.steps.title()) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + self.steps.go_back(); + } + Message::NextPressed => { + self.steps.advance(); + } + Message::StepMessage(step_msg) => { + self.steps.update(step_msg, &mut self.debug); + } + } + } + + fn view(&self) -> Element<Message> { + let Tour { steps, .. } = self; + + let mut controls = row(); + + if steps.has_previous() { + controls = controls.push( + button("Back") + .on_press(Message::BackPressed) + .style(style::Button::Secondary), + ); + } + + controls = controls.push(horizontal_space(Length::Fill)); + + if steps.can_continue() { + controls = controls.push( + button("Next") + .on_press(Message::NextPressed) + .style(style::Button::Primary), + ); + } + + let content: Element<_> = column() + .max_width(540) + .spacing(20) + .padding(20) + .push(steps.view(self.debug).map(Message::StepMessage)) + .push(controls) + .into(); + + let content = if self.debug { + // TODO + //content.explain(Color::BLACK) + content + } else { + content + }; + + let scrollable = + scrollable(container(content).width(Length::Fill).center_x()); + + container(scrollable).height(Length::Fill).center_y().into() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + BackPressed, + NextPressed, + StepMessage(StepMessage), +} + +struct Steps { + steps: Vec<Step>, + current: usize, +} + +impl Steps { + fn new() -> Steps { + Steps { + steps: vec![ + Step::Welcome, + Step::Slider { value: 50 }, + Step::RowsAndColumns { + layout: Layout::Row, + spacing: 20, + }, + Step::Text { + size: 30, + color: Color::BLACK, + }, + Step::Radio { selection: None }, + Step::Toggler { + can_continue: false, + }, + Step::Image { width: 300 }, + Step::Scrollable, + Step::TextInput { + value: String::new(), + is_secure: false, + }, + Step::Debugger, + Step::End, + ], + current: 0, + } + } + + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + self.steps[self.current].update(msg, debug); + } + + fn view(&self, debug: bool) -> Element<StepMessage> { + self.steps[self.current].view(debug) + } + + fn advance(&mut self) { + if self.can_continue() { + self.current += 1; + } + } + + fn go_back(&mut self) { + if self.has_previous() { + self.current -= 1; + } + } + + fn has_previous(&self) -> bool { + self.current > 0 + } + + fn can_continue(&self) -> bool { + self.current + 1 < self.steps.len() + && self.steps[self.current].can_continue() + } + + fn title(&self) -> &str { + self.steps[self.current].title() + } +} + +enum Step { + Welcome, + Slider { value: u8 }, + RowsAndColumns { layout: Layout, spacing: u16 }, + Text { size: u16, color: Color }, + Radio { selection: Option<Language> }, + Toggler { can_continue: bool }, + Image { width: u16 }, + Scrollable, + TextInput { value: String, is_secure: bool }, + Debugger, + End, +} + +#[derive(Debug, Clone)] +pub enum StepMessage { + SliderChanged(u8), + LayoutChanged(Layout), + SpacingChanged(u16), + TextSizeChanged(u16), + TextColorChanged(Color), + LanguageSelected(Language), + ImageWidthChanged(u16), + InputChanged(String), + ToggleSecureInput(bool), + DebugToggled(bool), + TogglerChanged(bool), +} + +impl<'a> Step { + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + match msg { + StepMessage::DebugToggled(value) => { + if let Step::Debugger = self { + *debug = value; + } + } + StepMessage::LanguageSelected(language) => { + if let Step::Radio { selection } = self { + *selection = Some(language); + } + } + StepMessage::SliderChanged(new_value) => { + if let Step::Slider { value, .. } = self { + *value = new_value; + } + } + StepMessage::TextSizeChanged(new_size) => { + if let Step::Text { size, .. } = self { + *size = new_size; + } + } + StepMessage::TextColorChanged(new_color) => { + if let Step::Text { color, .. } = self { + *color = new_color; + } + } + StepMessage::LayoutChanged(new_layout) => { + if let Step::RowsAndColumns { layout, .. } = self { + *layout = new_layout; + } + } + StepMessage::SpacingChanged(new_spacing) => { + if let Step::RowsAndColumns { spacing, .. } = self { + *spacing = new_spacing; + } + } + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width; + } + } + StepMessage::InputChanged(new_value) => { + if let Step::TextInput { value, .. } = self { + *value = new_value; + } + } + StepMessage::ToggleSecureInput(toggle) => { + if let Step::TextInput { is_secure, .. } = self { + *is_secure = toggle; + } + } + StepMessage::TogglerChanged(value) => { + if let Step::Toggler { can_continue, .. } = self { + *can_continue = value; + } + } + }; + } + + fn title(&self) -> &str { + match self { + Step::Welcome => "Welcome", + Step::Radio { .. } => "Radio button", + Step::Toggler { .. } => "Toggler", + Step::Slider { .. } => "Slider", + Step::Text { .. } => "Text", + Step::Image { .. } => "Image", + Step::RowsAndColumns { .. } => "Rows and columns", + Step::Scrollable => "Scrollable", + Step::TextInput { .. } => "Text input", + Step::Debugger => "Debugger", + Step::End => "End", + } + } + + fn can_continue(&self) -> bool { + match self { + Step::Welcome => true, + Step::Radio { selection } => *selection == Some(Language::Rust), + Step::Toggler { can_continue } => *can_continue, + Step::Slider { .. } => true, + Step::Text { .. } => true, + Step::Image { .. } => true, + Step::RowsAndColumns { .. } => true, + Step::Scrollable => true, + Step::TextInput { value, .. } => !value.is_empty(), + Step::Debugger => true, + Step::End => false, + } + } + + fn view(&self, debug: bool) -> Element<StepMessage> { + match self { + Step::Welcome => Self::welcome(), + Step::Radio { selection } => Self::radio(*selection), + Step::Toggler { can_continue } => Self::toggler(*can_continue), + Step::Slider { value } => Self::slider(*value), + Step::Text { size, color } => Self::text(*size, *color), + Step::Image { width } => Self::image(*width), + Step::RowsAndColumns { layout, spacing } => { + Self::rows_and_columns(*layout, *spacing) + } + Step::Scrollable => Self::scrollable(), + Step::TextInput { value, is_secure } => { + Self::text_input(value, *is_secure) + } + Step::Debugger => Self::debugger(debug), + Step::End => Self::end(), + } + .into() + } + + fn container(title: &str) -> Column<'a, StepMessage> { + column().spacing(20).push(text(title).size(50)) + } + + fn welcome() -> Column<'a, StepMessage> { + Self::container("Welcome!") + .push( + "This is a simple tour meant to showcase a bunch of widgets \ + that can be easily implemented on top of Iced.", + ) + .push( + "Iced is a cross-platform GUI library for Rust focused on \ + simplicity and type-safety. It is heavily inspired by Elm.", + ) + .push( + "It was originally born as part of Coffee, an opinionated \ + 2D game engine for Rust.", + ) + .push( + "On native platforms, Iced provides by default a renderer \ + built on top of wgpu, a graphics library supporting Vulkan, \ + Metal, DX11, and DX12.", + ) + .push( + "Additionally, this tour can also run on WebAssembly thanks \ + to dodrio, an experimental VDOM library for Rust.", + ) + .push( + "You will need to interact with the UI in order to reach the \ + end!", + ) + } + + fn slider(value: u8) -> Column<'a, StepMessage> { + Self::container("Slider") + .push( + "A slider allows you to smoothly select a value from a range \ + of values.", + ) + .push( + "The following slider lets you choose an integer from \ + 0 to 100:", + ) + .push(slider(0..=100, value, StepMessage::SliderChanged)) + .push( + text(value.to_string()) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn rows_and_columns( + layout: Layout, + spacing: u16, + ) -> Column<'a, StepMessage> { + let row_radio = + radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); + + let column_radio = radio( + "Column", + Layout::Column, + Some(layout), + StepMessage::LayoutChanged, + ); + + let layout_section: Element<_> = match layout { + Layout::Row => row() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + Layout::Column => column() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + }; + + let spacing_section = column() + .spacing(10) + .push(slider(0..=80, spacing, StepMessage::SpacingChanged)) + .push( + text(format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ); + + Self::container("Rows and columns") + .spacing(spacing) + .push( + "Iced uses a layout model based on flexbox to position UI \ + elements.", + ) + .push( + "Rows and columns can be used to distribute content \ + horizontally or vertically, respectively.", + ) + .push(layout_section) + .push("You can also easily change the spacing between elements:") + .push(spacing_section) + } + + fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + let size_section = column() + .padding(20) + .spacing(20) + .push("You can change its size:") + .push(text(format!("This text is {} pixels", size)).size(size)) + .push(Slider::new(10..=70, size, StepMessage::TextSizeChanged)); + + let color_sliders = row() + .spacing(10) + .push(color_slider(color.r, move |r| Color { r, ..color })) + .push(color_slider(color.g, move |g| Color { g, ..color })) + .push(color_slider(color.b, move |b| Color { b, ..color })); + + let color_section = column() + .padding(20) + .spacing(20) + .push("And its color:") + .push(text(format!("{:?}", color)).color(color)) + .push(color_sliders); + + Self::container("Text") + .push( + "Text is probably the most essential widget for your UI. \ + It will try to adapt to the dimensions of its container.", + ) + .push(size_section) + .push(color_section) + } + + fn radio(selection: Option<Language>) -> Column<'a, StepMessage> { + let question = column() + .padding(20) + .spacing(10) + .push(text("Iced is written in...").size(24)) + .push(Language::all().iter().cloned().fold( + column().padding(10).spacing(20), + |choices, language| { + choices.push(radio( + language, + language, + selection, + StepMessage::LanguageSelected, + )) + }, + )); + + Self::container("Radio button") + .push( + "A radio button is normally used to represent a choice... \ + Surprise test!", + ) + .push(question) + .push( + "Iced works very well with iterators! The list above is \ + basically created by folding a column over the different \ + choices, creating a radio button for each one of them!", + ) + } + + fn toggler(can_continue: bool) -> Column<'a, StepMessage> { + Self::container("Toggler") + .push("A toggler is mostly used to enable or disable something.") + .push( + Container::new(toggler( + "Toggle me to continue...".to_owned(), + can_continue, + StepMessage::TogglerChanged, + )) + .padding([0, 40]), + ) + } + + fn image(width: u16) -> Column<'a, StepMessage> { + Self::container("Image") + .push("An image that tries to keep its aspect ratio.") + .push(ferris(width)) + .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) + .push( + text(format!("Width: {} px", width.to_string())) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn scrollable() -> Column<'a, StepMessage> { + Self::container("Scrollable") + .push( + "Iced supports scrollable content. Try it out! Find the \ + button further below.", + ) + .push( + text("Tip: You can use the scrollbar to scroll down faster!") + .size(16), + ) + .push(vertical_space(Length::Units(4096))) + .push( + text("You are halfway there!") + .width(Length::Fill) + .size(30) + .horizontal_alignment(alignment::Horizontal::Center), + ) + .push(vertical_space(Length::Units(4096))) + .push(ferris(300)) + .push( + text("You made it!") + .width(Length::Fill) + .size(50) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { + let text_input = text_input( + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); + + Self::container("Text input") + .push("Use a text input to ask for different kinds of information.") + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(checkbox( + "Enable password mode", + is_secure, + StepMessage::ToggleSecureInput, + )) + .push( + "A text input produces a message every time it changes. It is \ + very easy to keep track of its contents:", + ) + .push( + text(if value.is_empty() { + "You have not typed anything yet..." + } else { + value + }) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } + + fn debugger(debug: bool) -> Column<'a, StepMessage> { + Self::container("Debugger") + .push( + "You can ask Iced to visually explain the layouting of the \ + different elements comprising your UI!", + ) + .push( + "Give it a shot! Check the following checkbox to be able to \ + see element boundaries.", + ) + .push(if cfg!(target_arch = "wasm32") { + Element::new( + text("Not available on web yet!") + .color([0.7, 0.7, 0.7]) + .horizontal_alignment(alignment::Horizontal::Center), + ) + } else { + checkbox("Explain layout", debug, StepMessage::DebugToggled) + .into() + }) + .push("Feel free to go back and take a look.") + } + + fn end() -> Column<'a, StepMessage> { + Self::container("You reached the end!") + .push("This tour will be updated as more features are added.") + .push("Make sure to keep an eye on it!") + } +} + +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { + container( + // This should go away once we unify resource loading on native + // platforms + if cfg!(target_arch = "wasm32") { + image("tour/images/ferris.png") + } else { + image(format!( + "{}/../../tour/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + } + .width(Length::Units(width)), + ) + .width(Length::Fill) + .center_x() +} + +fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + iced::pure::button( + text(label).horizontal_alignment(alignment::Horizontal::Center), + ) + .padding(12) + .width(Length::Units(100)) +} + +fn color_slider<'a>( + component: f32, + update: impl Fn(f32) -> Color + 'a, +) -> Slider<'a, f64, StepMessage> { + slider(0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Other, +} + +impl Language { + fn all() -> [Language; 6] { + [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Other, + ] + } +} + +impl From<Language> for String { + fn from(language: Language) -> String { + String::from(match language { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Other => "Other", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layout { + Row, + Column, +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + Secondary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + })), + border_radius: 12.0, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } +} diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index dc8a4de7..377d7a2d 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -105,8 +105,8 @@ impl Application for Stopwatch { Text::new(label) .horizontal_alignment(alignment::Horizontal::Center), ) - .min_width(80) .padding(10) + .width(Length::Units(80)) .style(style) }; diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index e199c88c..2024d25a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -763,7 +763,7 @@ fn button<'a, Message: Clone>( Text::new(label).horizontal_alignment(alignment::Horizontal::Center), ) .padding(12) - .min_width(100) + .width(Length::Units(100)) } fn color_slider( diff --git a/glow/src/lib.rs b/glow/src/lib.rs index 4e5a75d7..05435b54 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -22,7 +22,6 @@ mod text; mod triangle; pub mod settings; -pub mod widget; pub mod window; pub use backend::Backend; @@ -30,9 +29,6 @@ pub use settings::Settings; pub(crate) use iced_graphics::Transformation; -#[doc(no_inline)] -pub use widget::*; - pub use iced_graphics::{Error, Viewport}; pub use iced_native::alignment; diff --git a/glow/src/widget.rs b/glow/src/widget.rs deleted file mode 100644 index ee2810f9..00000000 --- a/glow/src/widget.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_glow::{button, Button}; -//! ``` -use crate::Renderer; - -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 rule; -pub mod scrollable; -pub mod slider; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -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; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - -pub use iced_native::widget::{Image, Space}; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message> = - iced_native::widget::Column<'a, Message, Renderer>; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message> = iced_native::widget::Row<'a, Message, Renderer>; - -/// A paragraph of text. -pub type Text = iced_native::widget::Text<Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs deleted file mode 100644 index f11ff25e..00000000 --- a/glow/src/widget/button.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::button::{Style, StyleSheet}; -pub use iced_native::widget::button::State; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message> = - iced_native::widget::Button<'a, Message, Renderer>; diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs deleted file mode 100644 index 399dd19c..00000000 --- a/glow/src/widget/canvas.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub use iced_graphics::canvas::*; diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs deleted file mode 100644 index 76d572d9..00000000 --- a/glow/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_graphics::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message> = - iced_native::widget::Checkbox<'a, Message, Renderer>; diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs deleted file mode 100644 index c16db50d..00000000 --- a/glow/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_graphics::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message> = - iced_native::widget::Container<'a, Message, Renderer>; diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs deleted file mode 100644 index 3c47acf0..00000000 --- a/glow/src/widget/pane_grid.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_graphics::pane_grid::{ - Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent, - Split, State, StyleSheet, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message> = - iced_native::widget::PaneGrid<'a, Message, Renderer>; - -/// The content of a [`Pane`]. -pub type Content<'a, Message> = - iced_native::widget::pane_grid::Content<'a, Message, Renderer>; - -/// The title bar of a [`Pane`]. -pub type TitleBar<'a, Message> = - iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/glow/src/widget/pick_list.rs b/glow/src/widget/pick_list.rs deleted file mode 100644 index 4d93be68..00000000 --- a/glow/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -pub use iced_native::widget::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::widget::PickList<'a, T, Message, crate::Renderer>; diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs deleted file mode 100644 index 413e6fb7..00000000 --- a/glow/src/widget/progress_bar.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. - -pub use iced_graphics::progress_bar::*; diff --git a/glow/src/widget/qr_code.rs b/glow/src/widget/qr_code.rs deleted file mode 100644 index 7b1c2408..00000000 --- a/glow/src/widget/qr_code.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Encode and display information in a QR code. -pub use iced_graphics::qr_code::*; diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs deleted file mode 100644 index 9ef1d7a5..00000000 --- a/glow/src/widget/radio.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_graphics::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message> = iced_native::widget::Radio<'a, Message, Renderer>; diff --git a/glow/src/widget/rule.rs b/glow/src/widget/rule.rs deleted file mode 100644 index 40281773..00000000 --- a/glow/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_graphics::rule::*; diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs deleted file mode 100644 index d5635ec5..00000000 --- a/glow/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; -pub use iced_native::widget::scrollable::State; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message> = - iced_native::widget::Scrollable<'a, Message, Renderer>; diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs deleted file mode 100644 index 2fb3d5d9..00000000 --- a/glow/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; -pub use iced_native::widget::slider::{Slider, State}; diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs deleted file mode 100644 index 5560e3e0..00000000 --- a/glow/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::text_input::{Style, StyleSheet}; -pub use iced_native::widget::text_input::State; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message> = - iced_native::widget::TextInput<'a, Message, Renderer>; diff --git a/glow/src/widget/toggler.rs b/glow/src/widget/toggler.rs deleted file mode 100644 index 40379025..00000000 --- a/glow/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_graphics::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message> = - iced_native::widget::Toggler<'a, Message, Renderer>; diff --git a/glow/src/widget/tooltip.rs b/glow/src/widget/tooltip.rs deleted file mode 100644 index c6af3903..00000000 --- a/glow/src/widget/tooltip.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Display a widget over another. -/// A widget allowing the selection of a single value from a list of options. -pub type Tooltip<'a, Message> = - iced_native::widget::Tooltip<'a, Message, crate::Renderer>; - -pub use iced_native::widget::tooltip::Position; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 8ccc7849..a84acbd6 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -17,6 +17,7 @@ font-source = ["font-kit"] font-fallback = [] font-icons = [] opengl = [] +pure = ["iced_pure"] [dependencies] glam = "0.10" @@ -35,6 +36,11 @@ path = "../native" version = "0.3" path = "../style" +[dependencies.iced_pure] +version = "0.1" +path = "../pure" +optional = true + [dependencies.lyon] version = "0.17" optional = true diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 7a32c850..93506258 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -1,12 +1,13 @@ //! Organize rendering primitives into a flattened list of layers. use crate::alignment; -use crate::image; -use crate::svg; use crate::triangle; use crate::{ Background, Font, Point, Primitive, Rectangle, Size, Vector, Viewport, }; +use iced_native::image; +use iced_native::svg; + /// A group of primitives that should be clipped together. #[derive(Debug, Clone)] pub struct Layer<'a> { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index c32eb471..cb31ea5f 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,8 +1,10 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; use crate::{Primitive, Vector}; +use iced_native::image; use iced_native::layout; use iced_native::renderer; +use iced_native::svg; use iced_native::text::{self, Text}; use iced_native::{Background, Element, Font, Point, Rectangle, Size}; @@ -168,3 +170,31 @@ where }); } } + +impl<B> image::Renderer for Renderer<B> +where + B: Backend + backend::Image, +{ + type Handle = image::Handle; + + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.backend().dimensions(handle) + } + + fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { + self.draw_primitive(Primitive::Image { handle, bounds }) + } +} + +impl<B> svg::Renderer for Renderer<B> +where + B: Backend + backend::Svg, +{ + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.backend().viewport_dimensions(handle) + } + + fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { + self.draw_primitive(Primitive::Svg { handle, bounds }) + } +} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index e34d267f..cf500a69 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -1,67 +1,4 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_graphics::{button, Button}; -//! ``` -pub mod button; -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 rule; -pub mod scrollable; -pub mod slider; -pub mod svg; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -mod column; -mod row; -mod space; -mod text; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -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; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -pub use column::Column; -pub use image::Image; -pub use row::Row; -pub use space::Space; -pub use svg::Svg; -pub use text::Text; - +//! Use the graphical widgets supported out-of-the-box. #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub mod canvas; @@ -77,3 +14,6 @@ pub mod qr_code; #[cfg(feature = "qr_code")] #[doc(no_inline)] pub use qr_code::QRCode; + +#[cfg(feature = "pure")] +pub mod pure; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs deleted file mode 100644 index 7b40c47b..00000000 --- a/graphics/src/widget/button.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_native::widget::button::{State, Style, StyleSheet}; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message, Backend> = - iced_native::widget::Button<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index 65d7e37e..6c526e35 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -6,14 +6,6 @@ use crate::renderer::{self, Renderer}; use crate::{Backend, Primitive}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, - Widget, -}; -use std::marker::PhantomData; - pub mod event; pub mod path; @@ -37,6 +29,15 @@ pub use program::Program; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use text::Text; +use iced_native::layout; +use iced_native::mouse; +use iced_native::{ + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, + Widget, +}; + +use std::marker::PhantomData; + /// A widget capable of drawing 2D graphics. /// /// # Examples @@ -97,7 +98,7 @@ pub struct Canvas<Message, P: Program<Message>> { width: Length, height: Length, program: P, - phantom: PhantomData<Message>, + message_: PhantomData<Message>, } impl<Message, P: Program<Message>> Canvas<Message, P> { @@ -109,7 +110,7 @@ impl<Message, P: Program<Message>> Canvas<Message, P> { width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), program, - phantom: PhantomData, + message_: PhantomData, } } diff --git a/graphics/src/widget/checkbox.rs b/graphics/src/widget/checkbox.rs deleted file mode 100644 index 0d2e93f9..00000000 --- a/graphics/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_style::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message, Backend> = - iced_native::widget::Checkbox<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs deleted file mode 100644 index 561681d5..00000000 --- a/graphics/src/widget/column.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::Renderer; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message, Backend> = - iced_native::widget::Column<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs deleted file mode 100644 index 99996f3b..00000000 --- a/graphics/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_style::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message, Backend> = - iced_native::widget::Container<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs deleted file mode 100644 index 76152484..00000000 --- a/graphics/src/widget/image.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Display images in your user interface. -pub mod viewer; - -use crate::backend::{self, Backend}; -use crate::{Primitive, Rectangle, Renderer}; - -use iced_native::image; - -pub use iced_native::widget::image::{Image, Viewer}; -pub use image::Handle; - -impl<B> image::Renderer for Renderer<B> -where - B: Backend + backend::Image, -{ - type Handle = image::Handle; - - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { - self.backend().dimensions(handle) - } - - fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Image { handle, bounds }) - } -} diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs deleted file mode 100644 index 9260990a..00000000 --- a/graphics/src/widget/image/viewer.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Zoom and pan on an image. -pub use iced_native::widget::image::Viewer; diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs deleted file mode 100644 index 95189920..00000000 --- a/graphics/src/widget/pane_grid.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_native::widget::pane_grid::{ - Axis, Configuration, Content, Direction, DragEvent, Node, Pane, - ResizeEvent, Split, State, TitleBar, -}; - -pub use iced_style::pane_grid::{Line, StyleSheet}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message, Backend> = - iced_native::widget::PaneGrid<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs deleted file mode 100644 index f3ac12b8..00000000 --- a/graphics/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -use crate::Renderer; - -pub use iced_native::widget::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::widget::PickList<'a, T, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs deleted file mode 100644 index 3666ecfd..00000000 --- a/graphics/src/widget/progress_bar.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. -pub use iced_native::widget::progress_bar::*; diff --git a/graphics/src/widget/pure.rs b/graphics/src/widget/pure.rs new file mode 100644 index 00000000..ee530379 --- /dev/null +++ b/graphics/src/widget/pure.rs @@ -0,0 +1,12 @@ +//! Leverage pure, virtual widgets in your application. +#[cfg(feature = "canvas")] +pub mod canvas; + +#[cfg(feature = "canvas")] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs new file mode 100644 index 00000000..2e3e7ede --- /dev/null +++ b/graphics/src/widget/pure/canvas.rs @@ -0,0 +1,237 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +mod program; + +pub use crate::widget::canvas::{Canvas as _, Program as _, *}; + +pub use program::Program; + +use crate::{Backend, Primitive, Renderer}; + +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size, Vector}; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub mod pure { +/// # pub use iced_graphics::pure::canvas; +/// # } +/// # pub use iced_native::{Color, Rectangle}; +/// # } +/// use iced::pure::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas<Message, P> +where + P: Program<Message>, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData<Message>, +} + +impl<Message, P> Canvas<Message, P> +where + P: Program<Message>, +{ + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } +} + +impl<Message, P, B> Widget<Message, Renderer<B>> for Canvas<Message, P> +where + P: Program<Message>, + B: Backend, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<P::State>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer<B>, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer<B>, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + iced_native::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer<B>, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer<B>, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let translation = Vector::new(bounds.x, bounds.y); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(Primitive::Group { + primitives: self + .program + .draw(state, bounds, cursor) + .into_iter() + .map(Geometry::into_primitive) + .collect(), + }); + }); + } +} + +impl<'a, Message, P, B> From<Canvas<Message, P>> + for Element<'a, Message, Renderer<B>> +where + Message: 'a, + P: Program<Message> + 'a, + B: Backend, +{ + fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer<B>> { + Element::new(canvas) + } +} diff --git a/graphics/src/widget/pure/canvas/program.rs b/graphics/src/widget/pure/canvas/program.rs new file mode 100644 index 00000000..ee74c27f --- /dev/null +++ b/graphics/src/widget/pure/canvas/program.rs @@ -0,0 +1,100 @@ +use crate::widget::pure::canvas::event::{self, Event}; +use crate::widget::pure::canvas::mouse; +use crate::widget::pure::canvas::{Cursor, Geometry}; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program<Message> { + /// The internal [`State`] mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the state of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry>; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl<Message, T> Program<Message> for &T +where + T: Program<Message>, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option<Message>) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { + T::draw(self, state, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/graphics/src/widget/pure/qr_code.rs b/graphics/src/widget/pure/qr_code.rs new file mode 100644 index 00000000..9d517374 --- /dev/null +++ b/graphics/src/widget/pure/qr_code.rs @@ -0,0 +1,61 @@ +//! Encode and display information in a QR code. +pub use crate::qr_code::*; + +use crate::{Backend, Renderer}; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::{Length, Point, Rectangle}; +use iced_pure::widget::tree::Tree; +use iced_pure::{Element, Widget}; + +impl<'a, Message, B> Widget<Message, Renderer<B>> for QRCode<'a> +where + B: Backend, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer<B>>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer<B>>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer<B>, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer<B>>>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer<B>, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer<B>>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for QRCode<'a> +where + B: Backend, +{ + fn into(self) -> Element<'a, Message, Renderer<B>> { + Element::new(self) + } +} diff --git a/graphics/src/widget/radio.rs b/graphics/src/widget/radio.rs deleted file mode 100644 index 20d72747..00000000 --- a/graphics/src/widget/radio.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_style::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message, Backend> = - iced_native::widget::Radio<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs deleted file mode 100644 index 5bee3fd5..00000000 --- a/graphics/src/widget/row.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::Renderer; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message, Backend> = - iced_native::widget::Row<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/rule.rs b/graphics/src/widget/rule.rs deleted file mode 100644 index b96924fa..00000000 --- a/graphics/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_native::widget::rule::*; diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs deleted file mode 100644 index 3fdaf668..00000000 --- a/graphics/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_native::widget::scrollable::State; -pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message, Backend> = - iced_native::widget::Scrollable<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs deleted file mode 100644 index 96dc6ec4..00000000 --- a/graphics/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_native::widget::slider::{Slider, State}; -pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs deleted file mode 100644 index 77e93dbb..00000000 --- a/graphics/src/widget/space.rs +++ /dev/null @@ -1 +0,0 @@ -pub use iced_native::widget::Space; diff --git a/graphics/src/widget/svg.rs b/graphics/src/widget/svg.rs deleted file mode 100644 index 5817a552..00000000 --- a/graphics/src/widget/svg.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Display vector graphics in your application. -use crate::backend::{self, Backend}; -use crate::{Primitive, Rectangle, Renderer}; -use iced_native::svg; - -pub use iced_native::widget::svg::Svg; -pub use svg::Handle; - -impl<B> svg::Renderer for Renderer<B> -where - B: Backend + backend::Svg, -{ - fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { - self.backend().viewport_dimensions(handle) - } - - fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Svg { handle, bounds }) - } -} diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs deleted file mode 100644 index 43516fca..00000000 --- a/graphics/src/widget/text.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Write some text for your users to read. -use crate::Renderer; - -/// A paragraph of text. -/// -/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. -pub type Text<Backend> = iced_native::widget::Text<Renderer<Backend>>; diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs deleted file mode 100644 index 87384d7e..00000000 --- a/graphics/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_native::widget::text_input::State; -pub use iced_style::text_input::{Style, StyleSheet}; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message, Backend> = - iced_native::widget::TextInput<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs deleted file mode 100644 index 9053e6ed..00000000 --- a/graphics/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_style::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled. -/// -/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message, Backend> = - iced_native::widget::Toggler<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs deleted file mode 100644 index 7dc12ed4..00000000 --- a/graphics/src/widget/tooltip.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` tooltip with a default -/// `Renderer`. -pub type Tooltip<'a, Message, Backend> = - iced_native::widget::Tooltip<'a, Message, Renderer<Backend>>; - -pub use iced_native::widget::tooltip::Position; diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index b840de50..2d7451f3 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -3,9 +3,17 @@ name = "iced_lazy" version = "0.1.0" edition = "2021" +[features] +pure = ["iced_pure"] + [dependencies] ouroboros = "0.13" [dependencies.iced_native] version = "0.4" path = "../native" + +[dependencies.iced_pure] +version = "0.1" +path = "../pure" +optional = true diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 05fce765..5d7d10e4 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -1,6 +1,9 @@ pub mod component; pub mod responsive; +#[cfg(feature = "pure")] +pub mod pure; + pub use component::Component; pub use responsive::Responsive; diff --git a/lazy/src/pure.rs b/lazy/src/pure.rs new file mode 100644 index 00000000..dc500e5e --- /dev/null +++ b/lazy/src/pure.rs @@ -0,0 +1,31 @@ +mod component; +mod responsive; + +pub use component::Component; +pub use responsive::Responsive; + +use iced_native::Size; +use iced_pure::Element; + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn component<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + component::view(component) +} + +pub fn responsive<'a, Message, Renderer>( + f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, +) -> Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + Responsive::new(f) +} diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs new file mode 100644 index 00000000..4d952f69 --- /dev/null +++ b/lazy/src/pure/component.rs @@ -0,0 +1,476 @@ +//! Build and reuse custom widgets using The Elm Architecture. +use iced_native::event; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size}; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use ouroboros::self_referencing; +use std::cell::{Ref, RefCell}; +use std::marker::PhantomData; + +/// A reusable, custom widget that uses The Elm Architecture. +/// +/// A [`Component`] allows you to implement custom widgets as if they were +/// `iced` applications with encapsulated state. +/// +/// In other words, a [`Component`] allows you to turn `iced` applications into +/// custom widgets and embed them without cumbersome wiring. +/// +/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) +/// and update the internal state of the [`Component`]. +/// +/// Additionally, a [`Component`] is capable of producing a `Message` to notify +/// the parent application of any relevant interactions. +pub trait Component<Message, Renderer> { + /// The internal state of this [`Component`]. + type State: Default; + + /// The type of event this [`Component`] handles internally. + type Event; + + /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. + /// + /// It can produce a `Message` for the parent application. + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + ) -> Option<Message>; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&self, state: &Self::State) -> Element<Self::Event, Renderer>; +} + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn view<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + Element::new(Instance { + state: RefCell::new(Some( + StateBuilder { + component: Box::new(component), + message: PhantomData, + state: PhantomData, + element_builder: |_| None, + } + .build(), + )), + }) +} + +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, +} + +#[self_referencing] +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>, + message: PhantomData<Message>, + state: PhantomData<S>, + + #[borrows(component)] + #[covariant] + element: Option<Element<'this, Event, Renderer>>, +} + +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where + S: Default, +{ + fn rebuild_element(&self, state: &S) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| Some(component.view(state)), + } + .build(), + ); + } + + fn with_element<T>( + &self, + f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, + ) -> T { + self.with_element_mut(|element| f(element)) + } + + fn with_element_mut<T>( + &self, + f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, + ) -> T { + self.state + .borrow_mut() + .as_mut() + .unwrap() + .with_element_mut(|element| f(element.as_mut().unwrap())) + } +} + +impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer> + for Instance<'a, Message, Renderer, Event, S> +where + S: 'static + Default, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<S>() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec<Tree> { + self.rebuild_element(&S::default()); + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + self.rebuild_element(tree.state.downcast_ref()); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }) + } + + fn width(&self) -> Length { + self.with_element(|element| element.as_widget().width()) + } + + fn height(&self) -> Length { + self.with_element(|element| element.as_widget().height()) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self.with_element_mut(|element| { + element.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ) + }); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if !local_messages.is_empty() { + let mut heads = self.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + self.state = RefCell::new(Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(tree.state.downcast_ref::<S>())) + }, + } + .build(), + )); + + shell.invalidate_layout(); + } + + event_status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.with_element(|element| { + element.as_widget().draw( + &tree.children[0], + renderer, + style, + layout, + cursor_position, + viewport, + ); + }); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_element(|element| { + element.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let overlay = OverlayBuilder { + instance: self, + instance_ref_builder: |instance| instance.state.borrow(), + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance + .as_ref() + .unwrap() + .borrow_element() + .as_ref() + .unwrap() + .as_widget() + .overlay(&mut tree.children[0], layout, renderer) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay.map(|position| { + overlay::Element::new( + position, + Box::new(OverlayInstance { + overlay: Some(overlay), + }), + ) + }) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Event, S> { + instance: &'a Instance<'b, Message, Renderer, Event, S>, + tree: &'a mut Tree, + types: PhantomData<(Message, Event, S)>, + + #[borrows(instance)] + #[covariant] + instance_ref: Ref<'this, Option<State<'a, Message, Renderer, Event, S>>>, + + #[borrows(instance_ref, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'this, Event, Renderer>>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { + overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>, +} + +impl<'a, 'b, Message, Renderer, Event, S> + OverlayInstance<'a, 'b, Message, Renderer, Event, S> +{ + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_ref() + .unwrap() + .borrow_overlay() + .as_ref() + .map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option<T> { + self.overlay + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer> + for OverlayInstance<'a, 'b, Message, Renderer, Event, S> +where + Renderer: iced_native::Renderer, + S: 'static + Default, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + let vector = position - overlay.position(); + + overlay.layout(renderer, bounds).translate(vector) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> iced_native::event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ) + }) + .unwrap_or_else(|| iced_native::event::Status::Ignored); + + local_shell.revalidate_layout(|| shell.invalidate_layout()); + + if !local_messages.is_empty() { + let overlay = self.overlay.take().unwrap().into_heads(); + let mut heads = overlay.instance.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(overlay.tree.state.downcast_mut::<S>(), message) + }) { + shell.publish(message); + } + + *overlay.instance.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |state| { + Some(state.view(overlay.tree.state.downcast_ref::<S>())) + }, + } + .build(), + ); + + self.overlay = Some( + OverlayBuilder { + instance: overlay.instance, + instance_ref_builder: |instance| instance.state.borrow(), + tree: overlay.tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance + .as_ref() + .unwrap() + .borrow_element() + .as_ref() + .unwrap() + .as_widget() + .overlay(tree, layout, renderer) + }, + } + .build(), + ); + + shell.invalidate_layout(); + } + + event_status + } +} diff --git a/lazy/src/pure/responsive.rs b/lazy/src/pure/responsive.rs new file mode 100644 index 00000000..2b62a047 --- /dev/null +++ b/lazy/src/pure/responsive.rs @@ -0,0 +1,381 @@ +use iced_native::event; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size}; +use iced_pure::horizontal_space; +use iced_pure::overlay; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use ouroboros::self_referencing; +use std::cell::{RefCell, RefMut}; +use std::marker::PhantomData; +use std::ops::Deref; + +/// A widget that is aware of its dimensions. +/// +/// A [`Responsive`] widget will always try to fill all the available space of +/// its parent. +#[allow(missing_debug_implementations)] +pub struct Responsive<'a, Message, Renderer> { + view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>, + content: RefCell<Content<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`Responsive`] widget with a closure that produces its + /// contents. + /// + /// The `view` closure will be provided with the current [`Size`] of + /// the [`Responsive`] widget and, therefore, can be used to build the + /// contents of the widget in a responsive way. + pub fn new( + view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, + ) -> Self { + Self { + view: Box::new(view), + content: RefCell::new(Content { + size: Size::ZERO, + layout: layout::Node::new(Size::ZERO), + element: Element::new(horizontal_space(Length::Units(0))), + }), + } + } +} + +struct Content<'a, Message, Renderer> { + size: Size, + layout: layout::Node, + element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> { + fn update( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + new_size: Size, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + ) { + if self.size == new_size { + return; + } + + self.element = view(new_size); + self.size = new_size; + self.layout = self + .element + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); + + tree.diff(&self.element); + } + + fn resolve<R, T>( + &mut self, + tree: &mut Tree, + renderer: R, + layout: Layout<'_>, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + f: impl FnOnce( + &mut Tree, + R, + Layout<'_>, + &mut Element<'a, Message, Renderer>, + ) -> T, + ) -> T + where + R: Deref<Target = Renderer>, + { + self.update(tree, renderer.deref(), layout.bounds().size(), view); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &self.layout, + ); + + f(tree, renderer, content_layout, &mut self.element) + } +} + +struct State { + tree: RefCell<Tree>, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + tree: RefCell::new(Tree::empty()), + }) + } + + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Fill + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(limits.max()) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget_mut().on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().draw( + tree, + renderer, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_ref::<State>(); + + let overlay = OverlayBuilder { + content: self.content.borrow_mut(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content, tree| { + content.update( + tree, + renderer, + layout.bounds().size(), + &self.view, + ); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &content.layout, + ); + + content.element.as_widget().overlay( + tree, + content_layout, + renderer, + ) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) + } +} + +impl<'a, Message, Renderer> From<Responsive<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: iced_native::Renderer + 'a, + Message: 'a, +{ + fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { + Self::new(responsive) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer> { + content: RefMut<'a, Content<'b, Message, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData<Message>, + + #[borrows(mut content, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'this, Message, Renderer>>, +} + +impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { + fn with_overlay_maybe<T>( + &self, + f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.borrow_overlay().as_ref().map(f) + } + + fn with_overlay_mut_maybe<T>( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option<T> { + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer> + for Overlay<'a, 'b, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.with_overlay_maybe(|overlay| { + let vector = position - overlay.position(); + + overlay.layout(renderer, bounds).translate(vector) + }) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, style, layout, cursor_position); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.with_overlay_mut_maybe(|overlay| { + overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .unwrap_or_else(|| iced_native::event::Status::Ignored) + } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 57fdd7d4..b03d9f27 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -61,8 +61,6 @@ pub struct Button<'a, Message, Renderer> { on_press: Option<Message>, width: Length, height: Length, - min_width: u32, - min_height: u32, padding: Padding, style_sheet: Box<dyn StyleSheet + 'a>, } @@ -84,8 +82,6 @@ where on_press: None, width: Length::Shrink, height: Length::Shrink, - min_width: 0, - min_height: 0, padding: Padding::new(5), style_sheet: Default::default(), } @@ -103,18 +99,6 @@ where self } - /// Sets the minimum width of the [`Button`]. - pub fn min_width(mut self, min_width: u32) -> Self { - self.min_width = min_width; - self - } - - /// Sets the minimum height of the [`Button`]. - pub fn min_height(mut self, min_height: u32) -> Self { - self.min_height = min_height; - self - } - /// Sets the [`Padding`] of the [`Button`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -151,6 +135,153 @@ impl State { } } +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +pub fn update<'a, Message: Clone>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_press: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if on_press.is_some() { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let state = state(); + + state.is_pressed = true; + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = state(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: crate::Renderer>( + renderer: &mut Renderer, + bounds: Rectangle, + cursor_position: Point, + is_enabled: bool, + style_sheet: &dyn StyleSheet, + state: impl FnOnce() -> &'a State, +) -> Style { + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if !is_enabled { + style_sheet.disabled() + } else if is_mouse_over { + let state = state(); + + if state.is_pressed { + style_sheet.pressed() + } else { + style_sheet.hovered() + } + } else { + style_sheet.active() + }; + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + border_radius: styling.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + styling +} + +/// Computes the layout of a [`Button`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height).pad(padding); + + let mut content = layout_content(renderer, &limits); + content.move_to(Point::new(padding.left.into(), padding.top.into())); + + let size = limits.resolve(content.size()).pad(padding); + + layout::Node::with_children(size, vec![content]) +} + +/// Returns the [`mouse::Interaction`] of a [`Button`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + is_enabled: bool, +) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + if is_mouse_over && is_enabled { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where @@ -170,22 +301,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .min_width(self.min_width) - .min_height(self.min_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - - let size = limits.resolve(content.size()).pad(self.padding); - - layout::Node::with_children(size, vec![content]) + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| self.content.layout(renderer, limits), + ) } fn on_event( @@ -208,42 +331,14 @@ where return event::Status::Captured; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if self.on_press.is_some() { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - self.state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { - let bounds = layout.bounds(); - - if self.state.is_pressed { - self.state.is_pressed = false; - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || &mut self.state, + ) } fn mouse_interaction( @@ -253,14 +348,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - if is_mouse_over && !is_disabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position, self.on_press.is_some()) } fn draw( @@ -274,51 +362,14 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); - let is_mouse_over = bounds.contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - let styling = if is_disabled { - self.style_sheet.disabled() - } else if is_mouse_over { - if self.state.is_pressed { - self.style_sheet.pressed() - } else { - self.style_sheet.hovered() - } - } else { - self.style_sheet.active() - }; - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - border_radius: styling.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: styling.border_radius, - border_width: styling.border_width, - border_color: styling.border_color, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } + let styling = draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + self.style_sheet.as_ref(), + || &self.state, + ); self.content.draw( renderer, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 15cbf93a..122c5e52 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -34,7 +34,7 @@ pub use iced_style::checkbox::{Style, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Checkbox<'a, Message, Renderer: text::Renderer> { is_checked: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: String, width: Length, size: u16, @@ -61,7 +61,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// `Message`. pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Checkbox { is_checked, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index ca85a425..0e7c301e 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -116,6 +116,32 @@ where } } +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.loose().width(width).height(height).pad(padding); + + let mut content = layout_content(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left.into(), padding.top.into())); + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ); + + layout::Node::with_children(size.pad(padding), vec![content]) +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where @@ -134,28 +160,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .loose() - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits.loose()); - let size = limits.resolve(content.size()); - - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - content.align( - Alignment::from(self.horizontal_alignment), - Alignment::from(self.vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(self.padding), vec![content]) + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| self.content.layout(renderer, limits), + ) } fn on_event( diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index de0ffbc0..8e7a28e5 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -65,6 +65,46 @@ impl<Handle> Image<Handle> { } } +/// Computes the layout of an [`Image`]. +pub fn layout<Renderer, Handle>( + renderer: &Renderer, + limits: &layout::Limits, + handle: &Handle, + width: Length, + height: Length, + content_fit: ContentFit, +) -> layout::Node +where + Renderer: image::Renderer<Handle = Handle>, +{ + // The raw w/h of the underlying image + let image_size = { + let (width, height) = renderer.dimensions(handle); + + Size::new(width as f32, height as f32) + }; + + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits.width(width).height(height).resolve(image_size); + + // The uncropped size of the image when fit to the bounds above + let full_size = content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) +} + impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, @@ -83,32 +123,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - // The raw w/h of the underlying image - let (width, height) = renderer.dimensions(&self.handle); - let image_size = Size::new(width as f32, height as f32); - - // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits - .width(self.width) - .height(self.height) - .resolve(image_size); - - // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); - - // Shrink the widget to fit the resized image, if requested - let final_size = Size { - width: match self.width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match self.height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) + layout( + renderer, + limits, + &self.handle, + self.width, + self.height, + self.content_fit, + ) } fn draw( diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 8ad63cf1..2093886e 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -11,16 +11,19 @@ mod axis; mod configuration; mod content; mod direction; +mod draggable; mod node; mod pane; mod split; -mod state; mod title_bar; +pub mod state; + pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; +pub use draggable::Draggable; pub use node::Node; pub use pane::Pane; pub use split::Split; @@ -92,6 +95,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; #[allow(missing_debug_implementations)] pub struct PaneGrid<'a, Message, Renderer> { state: &'a mut state::Internal, + action: &'a mut state::Action, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, @@ -124,6 +128,7 @@ where Self { state: &mut state.internal, + action: &mut state.action, elements, width: Length::Fill, height: Length::Fill, @@ -197,80 +202,407 @@ where } } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn click_pane( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) { - let mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| layout.bounds().contains(cursor_position), +/// Calculates the [`Layout`] of a [`PaneGrid`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + state: &state::Internal, + width: Length, + height: Length, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + layout_element: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + let size = limits.resolve(Size::ZERO); + + let regions = state.pane_regions(f32::from(spacing), size); + let children = elements + .filter_map(|(pane, element)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let mut node = layout_element( + element, + renderer, + &layout::Limits::new(size, size), ); - if let Some(((pane, content), layout)) = clicked_region.next() { - if let Some(on_click) = &self.on_click { - shell.publish(on_click(*pane)); + node.move_to(Point::new(region.x, region.y)); + + Some(node) + }) + .collect(); + + layout::Node::with_children(size, children) +} + +/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] +/// accordingly. +pub fn update<'a, Message, T: Draggable>( + action: &mut state::Action, + state: &state::Internal, + event: &Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: &Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +) -> event::Status { + let mut event_status = event::Status::Ignored; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + if action.picked_pane().is_none() { + *action = + state::Action::Resizing { split, axis }; + } + } else { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + let mut dropped_region = elements + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != target => { + DragEvent::Dropped { pane, target } + } + _ => DragEvent::Canceled { pane }, + }; + + shell.publish(on_drag(event)); + } + + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = on_resize { + if let Some((split, _)) = action.picked_split() { + let bounds = layout.bounds(); + + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), + ); - if let Some(on_drag) = &self.on_drag { - if content.can_be_picked_at(layout, cursor_position) { - let pane_position = layout.position(); + if let Some((axis, rectangle, _)) = splits.get(&split) { + let ratio = match axis { + Axis::Horizontal => { + let position = + cursor_position.y - bounds.y - rectangle.y; - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); + (position / rectangle.height).max(0.1).min(0.9) + } + Axis::Vertical => { + let position = + cursor_position.x - bounds.x - rectangle.x; - self.state.pick_pane(pane, origin); + (position / rectangle.width).max(0.1).min(0.9) + } + }; - shell.publish(on_drag(DragEvent::Picked { pane: *pane })); + shell.publish(on_resize(ResizeEvent { split, ratio })); + + event_status = event::Status::Captured; + } } } } + _ => {} } - fn trigger_resize( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let Some((_, on_resize)) = &self.on_resize { - if let Some((split, _)) = self.state.picked_split() { + event_status +} + +fn click_pane<'a, Message, T>( + action: &mut state::Action, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + elements: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, +) where + T: Draggable, +{ + let mut clicked_region = elements + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().contains(cursor_position)); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &on_click { + shell.publish(on_click(pane)); + } + + if let Some(on_drag) = &on_drag { + if content.can_be_dragged_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + *action = state::Action::Dragging { pane, origin }; + + shell.publish(on_drag(DragEvent::Picked { pane })); + } + } + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. +pub fn mouse_interaction( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + spacing: u16, + resize_leeway: Option<u16>, +) -> Option<mouse::Interaction> { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grab); + } + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { let bounds = layout.bounds(); - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, ); - if let Some((axis, rectangle, _)) = splits.get(&split) { - let ratio = match axis { - Axis::Horizontal => { - let position = - cursor_position.y - bounds.y - rectangle.y; + hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ) + .map(|(_, axis, _)| axis) + }) + }); - (position / rectangle.height).max(0.1).min(0.9) - } - Axis::Vertical => { - let position = - cursor_position.x - bounds.x - rectangle.x; + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } - (position / rectangle.width).max(0.1).min(0.9) - } - }; + None +} + +/// Draws a [`PaneGrid`]. +pub fn draw<Renderer, T>( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + renderer: &mut Renderer, + style: &renderer::Style, + viewport: &Rectangle, + spacing: u16, + resize_leeway: Option<u16>, + style_sheet: &dyn StyleSheet, + elements: impl Iterator<Item = (Pane, T)>, + draw_pane: impl Fn( + T, + &mut Renderer, + &renderer::Style, + Layout<'_>, + Point, + &Rectangle, + ), +) where + Renderer: crate::Renderer, +{ + let picked_pane = action.picked_pane(); - shell.publish(on_resize(ResizeEvent { split, ratio })); + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); - return event::Status::Captured; - } + let splits = state.split_regions(f32::from(spacing), bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + )?; + + Some((axis, region + Vector::new(bounds.x, bounds.y), false)) + } + None => None, + }); + + let pane_cursor_position = if picked_pane.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + for ((id, pane), layout) in elements.zip(layout.children()) { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + let bounds = layout.bounds(); + + renderer.with_translation( + cursor_position + - Point::new(bounds.x + origin.x, bounds.y + origin.y), + |renderer| { + renderer.with_layer(bounds, |renderer| { + draw_pane( + pane, + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); + }); + }, + ); + } + _ => { + draw_pane( + pane, + renderer, + style, + layout, + pane_cursor_position, + viewport, + ); } } + } - event::Status::Ignored + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + style_sheet.picked_split() + } else { + style_sheet.hovered_split() + }; + + if let Some(highlight) = highlight { + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + highlight.color, + ); + } } } @@ -331,28 +663,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - let regions = self.state.pane_regions(f32::from(self.spacing), size); - - let children = self - .elements - .iter() - .filter_map(|(pane, element)| { - let region = regions.get(pane)?; - let size = Size::new(region.width, region.height); - - let mut node = - element.layout(renderer, &layout::Limits::new(size, size)); - - node.move_to(Point::new(region.x, region.y)); - - Some(node) - }) - .collect(); - - layout::Node::with_children(size, children) + layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) } fn on_event( @@ -364,89 +684,21 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - event_status = event::Status::Captured; - - match self.on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - self.state.pick_split(&split, axis); - } else { - self.click_pane(layout, cursor_position, shell); - } - } - None => { - self.click_pane(layout, cursor_position, shell); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, _)) = self.state.picked_pane() { - if let Some(on_drag) = &self.on_drag { - let mut dropped_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != *target => { - DragEvent::Dropped { - pane, - target: *target, - } - } - _ => DragEvent::Canceled { pane }, - }; - - shell.publish(on_drag(event)); - } - - self.state.idle(); - - event_status = event::Status::Captured; - } else if self.state.picked_split().is_some() { - self.state.idle(); - - event_status = event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - event_status = - self.trigger_resize(layout, cursor_position, shell); - } - _ => {} - } - - let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + let event_status = update( + self.action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = self.action.picked_pane().map(|(pane, _)| pane); self.elements .iter_mut() @@ -474,53 +726,29 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - if self.state.picked_pane().is_some() { - return mouse::Interaction::Grab; - } - - let resize_axis = - self.state.picked_split().map(|(_, axis)| axis).or_else(|| { - self.on_resize.as_ref().and_then(|(leeway, _)| { - let bounds = layout.bounds(); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, + mouse_interaction( + self.action, + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(layout.children()) + .map(|((_pane, content), layout)| { + content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, ) - .map(|(_, axis, _)| axis) }) - }); - - if let Some(resize_axis) = resize_axis { - return match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }; - } - - self.elements - .iter() - .zip(layout.children()) - .map(|((_pane, content), layout)| { - content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() + .max() + .unwrap_or_default() + }) } fn draw( @@ -531,139 +759,22 @@ where cursor_position: Point, viewport: &Rectangle, ) { - let picked_pane = self.state.picked_pane(); - - let picked_split = self - .state - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds( - *region, - *ratio, - f32::from(self.spacing), - ); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match self.on_resize { - Some((leeway, _)) => { - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - )?; - - Some(( - axis, - region + Vector::new(bounds.x, bounds.y), - false, - )) - } - None => None, - }); - - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - for ((id, pane), layout) in self.elements.iter().zip(layout.children()) - { - match picked_pane { - Some((dragging, origin)) if *id == dragging => { - let bounds = layout.bounds(); - - renderer.with_translation( - cursor_position - - Point::new( - bounds.x + origin.x, - bounds.y + origin.y, - ), - |renderer| { - renderer.with_layer(bounds, |renderer| { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, - ); - }); - }, - ); - } - _ => { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, - ); - } - } - } - - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - self.style_sheet.picked_split() - } else { - self.style_sheet.hovered_split() - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) - / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); - } - } + draw( + self.action, + self.state, + layout, + cursor_position, + renderer, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style_sheet.as_ref(), + self.elements.iter().map(|(pane, content)| (*pane, content)), + |pane, renderer, style, layout, cursor_position, rectangle| { + pane.draw(renderer, style, layout, cursor_position, rectangle); + }, + ) } fn overlay( diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index 2320cb7c..02bde064 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -10,7 +10,9 @@ pub enum Axis { } impl Axis { - pub(super) fn split( + /// Splits the provided [`Rectangle`] on the current [`Axis`] with the + /// given `ratio` and `spacing`. + pub fn split( &self, rectangle: &Rectangle, ratio: f32, @@ -54,7 +56,8 @@ impl Axis { } } - pub(super) fn split_line_bounds( + /// Calculates the bounds of the split line in a [`Rectangle`] region. + pub fn split_line_bounds( &self, rectangle: Rectangle, ratio: f32, diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8b0e8d2a..f0ed0426 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -4,7 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; -use crate::widget::pane_grid::TitleBar; +use crate::widget::pane_grid::{Draggable, TitleBar}; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. @@ -101,23 +101,6 @@ where } } - /// Returns whether the [`Content`] with the given [`Layout`] can be picked - /// at the provided cursor position. - pub fn can_be_picked_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - title_bar.is_over_pick_area(title_bar_layout, cursor_position) - } else { - false - } - } - pub(crate) fn layout( &self, renderer: &Renderer, @@ -253,6 +236,26 @@ where } } +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } +} + impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> where T: Into<Element<'a, Message, Renderer>>, diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs new file mode 100644 index 00000000..6044871d --- /dev/null +++ b/native/src/widget/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +use crate::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { + /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked + /// at the provided cursor position. + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool; +} diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index feea0dec..f9ea21f4 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,3 +1,4 @@ +//! The state of a [`PaneGrid`]. use crate::widget::pane_grid::{ Axis, Configuration, Direction, Node, Pane, Split, }; @@ -19,8 +20,13 @@ use std::collections::{BTreeMap, HashMap}; /// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { - pub(super) panes: HashMap<Pane, T>, - pub(super) internal: Internal, + /// The panes of the [`PaneGrid`]. + pub panes: HashMap<Pane, T>, + + /// The internal state of the [`PaneGrid`]. + pub internal: Internal, + + pub(super) action: Action, } impl<T> State<T> { @@ -39,16 +45,13 @@ impl<T> State<T> { pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); - let (layout, last_id) = - Self::distribute_content(&mut panes, config.into(), 0); + let internal = + Internal::from_configuration(&mut panes, config.into(), 0); State { panes, - internal: Internal { - layout, - last_id, - action: Action::Idle, - }, + internal, + action: Action::Idle, } } @@ -192,16 +195,34 @@ impl<T> State<T> { None } } +} + +/// The internal state of a [`PaneGrid`]. +#[derive(Debug, Clone)] +pub struct Internal { + layout: Node, + last_id: usize, +} - fn distribute_content( +impl Internal { + /// Initializes the [`Internal`] state of a [`PaneGrid`] from a + /// [`Configuration`]. + pub fn from_configuration<T>( panes: &mut HashMap<Pane, T>, content: Configuration<T>, next_id: usize, - ) -> (Node, usize) { - match content { + ) -> Self { + let (layout, last_id) = match content { Configuration::Split { axis, ratio, a, b } => { - let (a, next_id) = Self::distribute_content(panes, *a, next_id); - let (b, next_id) = Self::distribute_content(panes, *b, next_id); + let Internal { + layout: a, + last_id: next_id, + } = Self::from_configuration(panes, *a, next_id); + + let Internal { + layout: b, + last_id: next_id, + } = Self::from_configuration(panes, *b, next_id); ( Node::Split { @@ -220,39 +241,53 @@ impl<T> State<T> { (Node::Pane(id), next_id + 1) } - } - } -} + }; -#[derive(Debug, Clone)] -pub struct Internal { - layout: Node, - last_id: usize, - action: Action, + Self { layout, last_id } + } } +/// The current action of a [`PaneGrid`]. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { + /// The [`PaneGrid`] is idle. Idle, - Dragging { pane: Pane, origin: Point }, - Resizing { split: Split, axis: Axis }, + /// A [`Pane`] in the [`PaneGrid`] is being dragged. + Dragging { + /// The [`Pane`] being dragged. + pane: Pane, + /// The starting [`Point`] of the drag interaction. + origin: Point, + }, + /// A [`Split`] in the [`PaneGrid`] is being dragged. + Resizing { + /// The [`Split`] being dragged. + split: Split, + /// The [`Axis`] of the [`Split`]. + axis: Axis, + }, } -impl Internal { +impl Action { + /// Returns the current [`Pane`] that is being dragged, if any. pub fn picked_pane(&self) -> Option<(Pane, Point)> { - match self.action { + match *self { Action::Dragging { pane, origin, .. } => Some((pane, origin)), _ => None, } } + /// Returns the current [`Split`] that is being dragged, if any. pub fn picked_split(&self) -> Option<(Split, Axis)> { - match self.action { + match *self { Action::Resizing { split, axis, .. } => Some((split, axis)), _ => None, } } +} +impl Internal { + /// Calculates the current [`Pane`] regions from the [`PaneGrid`] layout. pub fn pane_regions( &self, spacing: f32, @@ -261,6 +296,7 @@ impl Internal { self.layout.pane_regions(spacing, size) } + /// Calculates the current [`Split`] regions from the [`PaneGrid`] layout. pub fn split_regions( &self, spacing: f32, @@ -268,28 +304,4 @@ impl Internal { ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } - - pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { - self.action = Action::Dragging { - pane: *pane, - origin, - }; - } - - pub fn pick_split(&mut self, split: &Split, axis: Axis) { - // TODO: Obtain `axis` from layout itself. Maybe we should implement - // `Node::find_split` - if self.picked_pane().is_some() { - return; - } - - self.action = Action::Resizing { - split: *split, - axis, - }; - } - - pub fn idle(&mut self) { - self.action = Action::Idle; - } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 3be6c20c..e050ada5 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -23,11 +23,7 @@ pub struct PickList<'a, T, Message, Renderer: text::Renderer> where [T]: ToOwned<Owned = Vec<T>>, { - menu: &'a mut menu::State, - keyboard_modifiers: &'a mut keyboard::Modifiers, - is_open: &'a mut bool, - hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + state: &'a mut State<T>, on_selected: Box<dyn Fn(T) -> Message>, options: Cow<'a, [T]>, placeholder: Option<String>, @@ -49,8 +45,9 @@ pub struct State<T> { last_selection: Option<T>, } -impl<T> Default for State<T> { - fn default() -> Self { +impl<T> State<T> { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { Self { menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), @@ -61,6 +58,12 @@ impl<T> Default for State<T> { } } +impl<T> Default for State<T> { + fn default() -> Self { + Self::new() + } +} + impl<'a, T: 'a, Message, Renderer: text::Renderer> PickList<'a, T, Message, Renderer> where @@ -79,20 +82,8 @@ where selected: Option<T>, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { - let State { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, - } = state; - Self { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, + state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -145,128 +136,118 @@ where } } -impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> - for PickList<'a, T, Message, Renderer> +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, - Renderer: text::Renderer + 'a, + Renderer: text::Renderer, + T: ToString, { - fn width(&self) -> Length { - self.width - } + use std::f32; - fn height(&self) -> Length { - Length::Shrink - } + let limits = limits.width(width).height(Length::Shrink).pad(padding); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits - .width(self.width) - .height(Length::Shrink) - .pad(self.padding); - - let text_size = self.text_size.unwrap_or(renderer.default_size()); - let font = self.font.clone(); - - let max_width = match self.width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let (width, _) = renderer.measure( - label, - text_size, - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }; + let text_size = text_size.unwrap_or(renderer.default_size()); - let labels = self.options.iter().map(ToString::to_string); + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font.clone(), + Size::new(f32::INFINITY, f32::INFINITY), + ); - let labels_width = - labels.map(|label| measure(&label)).max().unwrap_or(100); + width.round() as u32 + }; - let placeholder_width = self - .placeholder - .as_ref() - .map(String::as_str) - .map(measure) - .unwrap_or(100); + let labels = options.iter().map(ToString::to_string); - labels_width.max(placeholder_width) - } - _ => 0, - }; + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); - let size = { - let intrinsic = Size::new( - max_width as f32 - + f32::from(text_size) - + f32::from(self.padding.left), - f32::from(text_size), - ); + let placeholder_width = placeholder.map(measure).unwrap_or(100); - limits.resolve(intrinsic).pad(self.padding) - }; + labels_width.max(placeholder_width) + } + _ => 0, + }; - layout::Node::new(size) - } + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size) + f32::from(padding.left), + f32::from(text_size), + ); - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let event_status = if *self.is_open { - // TODO: Encode cursor availability in the type system - *self.is_open = - cursor_position.x < 0.0 || cursor_position.y < 0.0; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - let selected = self.selected.as_ref(); - - *self.is_open = true; - *self.hovered_option = self - .options - .iter() - .position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; + limits.resolve(intrinsic).pad(padding) + }; + + layout::Node::new(size) +} - if let Some(last_selection) = self.last_selection.take() { - shell.publish((self.on_selected)(last_selection)); +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State<T>, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); - *self.is_open = false; + let event_status = if state.is_open { + // TODO: Encode cursor availability in the type system + state.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; - event::Status::Captured - } else { - event_status - } + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) if self.keyboard_modifiers.command() + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() && layout.bounds().contains(cursor_position) - && !*self.is_open => + && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, @@ -278,34 +259,219 @@ where } let next_option = if y < 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter()) + if let Some(selected) = selected { + find_next(selected, options.iter()) } else { - self.options.first() + options.first() } } else if y > 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter().rev()) + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) } else { - self.options.last() + options.last() } } else { None }; if let Some(next_option) = next_option { - shell.publish((self.on_selected)(next_option.clone())); + shell.publish((on_selected)(next_option.clone())); } event::Status::Captured - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - *self.keyboard_modifiers = modifiers; - + } else { event::Status::Ignored } - _ => event::Status::Ignored, } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State<T>, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + options: &'a [T], + style_sheet: &dyn StyleSheet, +) -> Option<overlay::Element<'a, Message, Renderer>> +where + Message: 'a, + Renderer: text::Renderer + 'a, + T: Clone + ToString, +{ + if state.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut state.menu, + options, + &mut state.hovered_option, + &mut state.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(padding) + .font(font) + .style(style_sheet.menu()); + + if let Some(text_size) = text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } +} + +/// Draws a [`PickList`]. +pub fn draw<T, Renderer>( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + style_sheet: &dyn StyleSheet, +) where + Renderer: text::Renderer, + T: ToString, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let is_selected = selected.is_some(); + + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + renderer.fill_text(Text { + content: &Renderer::ARROW_DOWN_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding.horizontal()), + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + + let label = selected.map(ToString::to_string); + + if let Some(label) = + label.as_ref().map(String::as_str).or_else(|| placeholder) + { + renderer.fill_text(Text { + content: label, + size: f32::from(text_size.unwrap_or(renderer.default_size())), + font: font.clone(), + color: is_selected + .then(|| style.text_color) + .unwrap_or(style.placeholder_color), + bounds: Rectangle { + x: bounds.x + f32::from(padding.left), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }) + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || &mut self.state, + ) } fn mouse_interaction( @@ -315,14 +481,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( @@ -333,66 +492,17 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = self.selected.is_some(); - - let style = if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius, - }, - style.background, - ); - - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - - f32::from(self.padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - - if let Some(label) = self - .selected - .as_ref() - .map(ToString::to_string) - .as_ref() - .or_else(|| self.placeholder.as_ref()) - { - renderer.fill_text(Text { - content: label, - size: f32::from( - self.text_size.unwrap_or(renderer.default_size()), - ), - font: self.font.clone(), - color: is_selected - .then(|| style.text_color) - .unwrap_or(style.placeholder_color), - bounds: Rectangle { - x: bounds.x + f32::from(self.padding.left), - y: bounds.center_y(), - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }) - } + draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) } fn overlay( @@ -400,28 +510,15 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option<overlay::Element<'_, Message, Renderer>> { - if *self.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut self.menu, - &self.options, - &mut self.hovered_option, - &mut self.last_selection, - ) - .width(bounds.width.round() as u16) - .padding(self.padding) - .font(self.font.clone()) - .style(self.style_sheet.menu()); - - if let Some(text_size) = self.text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } + overlay( + layout, + &mut self.state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) } } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index fed2925b..657ae786 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -79,7 +79,7 @@ where ) -> Self where V: Eq + Copy, - F: 'static + Fn(V) -> Message, + F: FnOnce(V) -> Message, { Radio { is_selected: Some(value) == selected, diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index b0cc3768..69619583 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -15,20 +15,20 @@ pub struct Rule<'a> { } impl<'a> Rule<'a> { - /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. - pub fn horizontal(spacing: u16) -> Self { + /// Creates a horizontal [`Rule`] with the given height. + pub fn horizontal(height: u16) -> Self { Rule { width: Length::Fill, - height: Length::from(Length::Units(spacing)), + height: Length::Units(height), is_horizontal: true, style_sheet: Default::default(), } } - /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. - pub fn vertical(spacing: u16) -> Self { + /// Creates a vertical [`Rule`] with the given width. + pub fn vertical(width: u16) -> Self { Rule { - width: Length::from(Length::Units(spacing)), + width: Length::from(Length::Units(width)), height: Length::Fill, is_horizontal: false, style_sheet: Default::default(), diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ce734ad8..748fd27d 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -15,6 +15,13 @@ use std::{f32, u32}; pub use iced_style::scrollable::StyleSheet; +pub mod style { + //! The styles of a [`Scrollable`]. + //! + //! [`Scrollable`]: crate::widget::Scrollable + pub use iced_style::scrollable::{Scrollbar, Scroller}; +} + /// A widget that can vertically display an infinite amount of content with a /// scrollbar. #[allow(missing_debug_implementations)] @@ -139,235 +146,201 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } - - fn notify_on_scroll( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - shell: &mut Shell<'_, Message>, - ) { - if content_bounds.height <= bounds.height { - return; - } - - if let Some(on_scroll) = &self.on_scroll { - shell.publish(on_scroll( - self.state.offset.absolute(bounds, content_bounds) - / (content_bounds.height - bounds.height), - )); - } - } - - fn scrollbar( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Option<Scrollbar> { - let offset = self.state.offset(bounds, content_bounds); - - if content_bounds.height > bounds.height { - let outer_width = self.scrollbar_width.max(self.scroller_width) - + 2 * self.scrollbar_margin; - - let outer_bounds = Rectangle { - x: bounds.x + bounds.width - outer_width as f32, - y: bounds.y, - width: outer_width as f32, - height: bounds.height, - }; - - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scrollbar_width / 2), - y: bounds.y, - width: self.scrollbar_width as f32, - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scroller_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scroller_width / 2), - y: scrollbar_bounds.y + y_offset, - width: self.scroller_width as f32, - height: scroller_height, - }; - - Some(Scrollbar { - outer_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } - } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn width(&self) -> Length { - Widget::<Message, Renderer>::width(&self.content) - } - - fn height(&self) -> Length { - self.height - } +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .max_height(self.max_height) - .width(Widget::<Message, Renderer>::width(&self.content)) - .height(self.height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), - Size::new(limits.max().width, f32::INFINITY), - ); + let child_limits = layout::Limits::new( + Size::new(limits.min().width, 0.0), + Size::new(limits.max().width, f32::INFINITY), + ); - let content = self.content.layout(renderer, &child_limits); - let size = limits.resolve(content.size()); + let content = layout_content(renderer, &child_limits); + let size = limits.resolve(content.size()); - layout::Node::with_children(size, vec![content]) - } + layout::Node::with_children(size, vec![content]) +} - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbar = self.scrollbar(bounds, content_bounds); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event.clone(), - content, - cursor_position, - renderer, - clipboard, - shell, +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update<Message>( + state: &mut State, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + Point, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + state.offset(bounds, content_bounds) as f32, ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) }; - if let event::Status::Captured = event_status { - return event::Status::Captured; - } + update_content( + event.clone(), + content, + cursor_position, + clipboard, + shell, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if is_mouse_over { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + // TODO: Configurable speed (?) + state.scroll(y * 60.0, bounds, content_bounds); + } + mouse::ScrollDelta::Pixels { y, .. } => { + state.scroll(y, bounds, content_bounds); + } + } - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - self.state.scroll(y * 60.0, bounds, content_bounds); - } - mouse::ScrollDelta::Pixels { y, .. } => { - self.state.scroll(y, bounds, content_bounds); - } + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + state.scroll_box_touched_at = Some(cursor_position); } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroll(delta, bounds, content_bounds); - return event::Status::Captured; - } - Event::Touch(event) => { - match event { - touch::Event::FingerPressed { .. } => { - self.state.scroll_box_touched_at = - Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - self.state.scroll_box_touched_at - { - let delta = - cursor_position.y - scroll_box_touched_at.y; - - self.state.scroll( - delta, - bounds, - content_bounds, - ); - - self.state.scroll_box_touched_at = - Some(cursor_position); - - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - self.state.scroll_box_touched_at = None; + state.scroll_box_touched_at = Some(cursor_position); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_box_touched_at = None; + } } - _ => {} + + return event::Status::Captured; } + _ => {} } + } - if self.state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.scroller_grabbed_at = None; + if state.is_scroller_grabbed() { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.scroller_grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, state.scroller_grabbed_at) + { + state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, self.state.scroller_grabbed_at) + } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) { - self.state.scroll_to( + state.scroll_to( scrollbar.scroll_percentage( scroller_grabbed_at, cursor_position, @@ -376,50 +349,329 @@ where content_bounds, ); - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } } - _ => {} } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - self.state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); + _ => {} + } + } - self.state.scroller_grabbed_at = - Some(scroller_grabbed_at); + event::Status::Ignored +} - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( + state: &State, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + content_interaction: impl FnOnce( + Layout<'_>, + Point, + &Rectangle, + ) -> mouse::Interaction, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + let offset = state.offset(bounds, content_bounds); - return event::Status::Captured; - } - } + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) + }; + + content_interaction( + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ) + } +} + +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( + state: &State, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + style_sheet: &dyn StyleSheet, + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), +) where + Renderer: crate::Renderer, +{ + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) + }; + + if let Some(scrollbar) = scrollbar { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(0.0, -(offset as f32)), + |renderer| { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + }, + ); + }); + + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0.0; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if is_scrollbar_visible { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); } - _ => {} - } - } - event::Status::Ignored + if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + }, + ); + } else { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + } +} + +fn scrollbar( + state: &State, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + bounds: Rectangle, + content_bounds: Rectangle, +) -> Option<Scrollbar> { + let offset = state.offset(bounds, content_bounds); + + if content_bounds.height > bounds.height { + let outer_width = + scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + + let outer_bounds = Rectangle { + x: bounds.x + bounds.width - outer_width as f32, + y: bounds.y, + width: outer_width as f32, + height: bounds.height, + }; + + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scrollbar_width / 2), + y: bounds.y, + width: scrollbar_width as f32, + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scroller_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scroller_width / 2), + y: scrollbar_bounds.y + y_offset, + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + outer_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } +} + +fn notify_on_scroll<Message>( + state: &State, + on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = on_scroll { + shell.publish(on_scroll( + state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + Widget::<Message, Renderer>::width(&self.content) + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + |renderer, limits| self.content.layout(renderer, limits), + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + &mut self.state, + event, + layout, + cursor_position, + clipboard, + shell, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) } fn mouse_interaction( @@ -429,38 +681,22 @@ where _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() { - mouse::Interaction::Idle - } else { - let offset = self.state.offset(bounds, content_bounds); - - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) - } else { - Point::new(cursor_position.x, -1.0) - }; - - self.content.mouse_interaction( - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - renderer, - ) - } + mouse_interaction( + &self.state, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) } fn draw( @@ -471,103 +707,25 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) - } else { - Point::new(cursor_position.x, -1.0) - }; - - if let Some(scrollbar) = scrollbar { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(0.0, -(offset as f32)), - |renderer| { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - }, - ); - }); - - let style = if self.state.is_scroller_grabbed() { - self.style_sheet.dragging() - } else if is_mouse_over_scrollbar { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - if is_scrollbar_visible { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background.unwrap_or(Background::Color( - Color::TRANSPARENT, - )), - ); - } - - if is_mouse_over - || self.state.is_scroller_grabbed() - || is_scrollbar_visible - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius, - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } - }, - ); - } else { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - } + draw( + &self.state, + renderer, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style_sheet.as_ref(), + |renderer, layout, cursor_position, viewport| { + self.content.draw( + renderer, + style, + layout, + cursor_position, + viewport, + ) + }, + ) } fn overlay( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 289f75f5..4c56083e 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -142,6 +142,207 @@ where } } +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option<Message>, +) -> event::Status +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = || { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + *range.start() + } else if cursor_position.x >= bounds.x + bounds.width { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T>( + renderer: &mut impl crate::Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet, +) where + T: Into<f64> + Copy, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), f32::from(bounds.height), border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + /// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -192,73 +393,18 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let is_dragging = self.state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *self.range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *self.range.end() - } else { - let step = self.step.into(); - let start = (*self.range.start()).into(); - let end = (*self.range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - if (self.value.into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((self.on_change)(new_value)); - - self.value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = self.on_release.clone() { - shell.publish(on_release); - } - self.state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + &mut self.state, + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) } fn draw( @@ -269,90 +415,15 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_dragging { - self.style_sheet.dragging() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 2.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), f32::from(bounds.height), border_radius), - }; - - let value = self.value.into() as f32; - let (range_start, range_end) = { - let (start, end) = self.range.clone().into_inner(); - - (start.into() as f32, end.into() as f32) - }; - - let handle_offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) - }; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border_radius: handle_border_radius, - border_width: style.handle.border_width, - border_color: style.handle.border_color, - }, - style.handle.color, - ); + draw( + renderer, + layout, + cursor_position, + &self.state, + self.value, + &self.range, + self.style_sheet.as_ref(), + ) } fn mouse_interaction( @@ -362,16 +433,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if self.state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position, &self.state) } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e30e2343..057f34c6 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -24,8 +24,6 @@ use crate::{ Shell, Size, Vector, Widget, }; -use std::u32; - pub use iced_style::text_input::{Style, StyleSheet}; /// A field that can be filled with text. @@ -61,10 +59,9 @@ pub struct TextInput<'a, Message, Renderer: text::Renderer> { is_secure: bool, font: Renderer::Font, width: Length, - max_width: u32, padding: Padding, size: Option<u16>, - on_change: Box<dyn Fn(String) -> Message>, + on_change: Box<dyn Fn(String) -> Message + 'a>, on_submit: Option<Message>, style_sheet: Box<dyn StyleSheet + 'a>, } @@ -88,7 +85,7 @@ where on_change: F, ) -> Self where - F: 'static + Fn(String) -> Message, + F: 'a + Fn(String) -> Message, { TextInput { state, @@ -97,7 +94,6 @@ where is_secure: false, font: Default::default(), width: Length::Fill, - max_width: u32::MAX, padding: Padding::ZERO, size: None, on_change: Box::new(on_change), @@ -126,12 +122,6 @@ where self } - /// Sets the maximum width of the [`TextInput`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - /// Sets the [`Padding`] of the [`TextInput`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -164,12 +154,7 @@ where pub fn state(&self) -> &State { self.state } -} -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> -where - Renderer: text::Renderer, -{ /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. pub fn draw( @@ -179,507 +164,328 @@ where cursor_position: Point, value: Option<&Value>, ) { - let value = value.unwrap_or(&self.value); - let secure_value = self.is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(&value); - - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_focused() { - self.style_sheet.focused() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background, - ); - - let text = value.to_string(); - let size = self.size.unwrap_or(renderer.default_size()); - - let (cursor, offset) = if self.state.is_focused() { - match self.state.cursor.state(&value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - position, - self.font.clone(), - ); - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.value_color(), - )), - offset, - ) - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - let (left_position, left_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - left, - self.font.clone(), - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - right, - self.font.clone(), - ); - - let width = right_position - left_position; - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.selection_color(), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let text_width = renderer.measure_width( - if text.is_empty() { - &self.placeholder - } else { - &text - }, - size, - self.font.clone(), - ); - - let render = |renderer: &mut Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); - } - - renderer.fill_text(Text { - content: if text.is_empty() { - &self.placeholder - } else { - &text - }, - color: if text.is_empty() { - self.style_sheet.placeholder_color() - } else { - self.style_sheet.value_color() - }, - font: self.font.clone(), - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size: f32::from(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - }; - - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render) - }); - } else { - render(renderer); - } + draw( + renderer, + layout, + cursor_position, + &self.state, + value.unwrap_or(&self.value), + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style_sheet.as_ref(), + ) } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for TextInput<'a, Message, Renderer> +/// Computes the layout of a [`TextInput`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option<u16>, +) -> layout::Node where - Message: Clone, Renderer: text::Renderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let text_size = self.size.unwrap_or(renderer.default_size()); - - let limits = limits - .pad(self.padding) - .width(self.width) - .max_width(self.max_width) - .height(Length::Units(text_size)); - - let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - - layout::Node::with_children(text.size().pad(self.padding), vec![text]) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let is_clicked = layout.bounds().contains(cursor_position); + let text_size = size.unwrap_or(renderer.default_size()); - self.state.is_focused = is_clicked; + let limits = limits + .pad(padding) + .width(width) + .height(Length::Units(text_size)); - if is_clicked { - let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; + let mut text = layout::Node::new(limits.resolve(Size::ZERO)); + text.move_to(Point::new(padding.left.into(), padding.top.into())); - let click = mouse::Click::new( - cursor_position, - self.state.last_click, - ); + layout::Node::with_children(text.size().pad(padding), vec![text]) +} - match click.kind() { - click::Kind::Single => { - let position = if target > 0.0 { - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - - find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +pub fn update<'a, Message, Renderer>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + on_change: &dyn Fn(String) -> Message, + on_submit: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status +where + Message: Clone, + Renderer: text::Renderer, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + let is_clicked = layout.bounds().contains(cursor_position); + + state.is_focused = is_clicked; + + if is_clicked { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + let click = + mouse::Click::new(cursor_position, state.last_click); + + match click.kind() { + click::Kind::Single => { + let position = if target > 0.0 { + let value = if is_secure { + value.secure() } else { - None + value.clone() }; - self.state.cursor.move_to(position.unwrap_or(0)); - self.state.is_dragging = true; - } - click::Kind::Double => { - if self.is_secure { - self.state.cursor.select_all(&self.value); - } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &self.value, - &self.state, - target, - ) - .unwrap_or(0); - - self.state.cursor.select_range( - self.value.previous_start_of_word(position), - self.value.next_end_of_word(position), - ); - } + find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + } else { + None + }; - self.state.is_dragging = false; - } - click::Kind::Triple => { - self.state.cursor.select_all(&self.value); - self.state.is_dragging = false; + state.cursor.move_to(position.unwrap_or(0)); + state.is_dragging = true; + } + click::Kind::Double => { + if is_secure { + state.cursor.select_all(value); + } else { + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(value); + state.is_dragging = false; } + } - self.state.last_click = Some(click); + state.last_click = Some(click); - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_dragging = false; + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - if self.state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state().is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state(); - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) - .unwrap_or(0); + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; - self.state.cursor.select_range( - self.state.cursor.start(&value), - position, - ); + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); - return event::Status::Captured; - } + return event::Status::Captured; } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) - if self.state.is_focused - && self.state.is_pasting.is_none() - && !self.state.keyboard_modifiers.command() - && !c.is_control() => + } + Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { + let state = state(); + + if state.is_focused + && state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() { - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new(value, &mut state.cursor); editor.insert(c); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyPressed { - key_code, .. - }) if self.state.is_focused => { - let modifiers = self.state.keyboard_modifiers; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + let state = state(); + + if state.is_focused { + let modifiers = state.keyboard_modifiers; match key_code { keyboard::KeyCode::Enter | keyboard::KeyCode::NumpadEnter => { - if let Some(on_submit) = self.on_submit.clone() { + if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } keyboard::KeyCode::Backspace => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(0, cursor_pos); } else { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.backspace(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state + if is_secure { + let cursor_pos = state.cursor.end(value); + state .cursor - .select_range(cursor_pos, self.value.len()); + .select_range(cursor_pos, value.len()); } else { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } else { - self.state - .cursor - .move_left_by_words(&self.value); + state.cursor.move_left_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_left(&self.value) + state.cursor.select_left(value) } else { - self.state.cursor.move_left(&self.value); + state.cursor.move_left(value); } } keyboard::KeyCode::Right => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } else { - self.state - .cursor - .move_right_by_words(&self.value); + state.cursor.move_right_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_right(&self.value) + state.cursor.select_right(value) } else { - self.state.cursor.move_right(&self.value); + state.cursor.move_right(value); } } keyboard::KeyCode::Home => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); + state + .cursor + .select_range(state.cursor.start(value), 0); } else { - self.state.cursor.move_to(0); + state.cursor.move_to(0); } } keyboard::KeyCode::End => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), + state.cursor.select_range( + state.cursor.start(value), + value.len(), ); } else { - self.state.cursor.move_to(self.value.len()); + state.cursor.move_to(value.len()); } } keyboard::KeyCode::C - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { + match state.cursor.selection(value) { Some((start, end)) => { clipboard.write( - self.value.select(start, end).to_string(), + value.select(start, end).to_string(), ); } None => {} } } keyboard::KeyCode::X - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { + match state.cursor.selection(value) { Some((start, end)) => { clipboard.write( - self.value.select(start, end).to_string(), + value.select(start, end).to_string(), ); } None => {} } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::V => { - if self.state.keyboard_modifiers.command() { - let content = match self.state.is_pasting.take() { + if state.keyboard_modifiers.command() { + let content = match state.is_pasting.take() { Some(content) => content, None => { let content: String = clipboard @@ -693,32 +499,30 @@ where } }; - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); + let mut editor = + Editor::new(value, &mut state.cursor); editor.paste(content.clone()); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); - self.state.is_pasting = Some(content); + state.is_pasting = Some(content); } else { - self.state.is_pasting = None; + state.is_pasting = None; } } keyboard::KeyCode::A - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - self.state.cursor.select_all(&self.value); + state.cursor.select_all(value); } keyboard::KeyCode::Escape => { - self.state.is_focused = false; - self.state.is_dragging = false; - self.state.is_pasting = None; + state.is_focused = false; + state.is_dragging = false; + state.is_pasting = None; - self.state.keyboard_modifiers = + state.keyboard_modifiers = keyboard::Modifiers::default(); } keyboard::KeyCode::Tab @@ -728,15 +532,17 @@ where } _ => {} } - - return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyReleased { - key_code, .. - }) if self.state.is_focused => { + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + let state = state(); + + if state.is_focused { match key_code { keyboard::KeyCode::V => { - self.state.is_pasting = None; + state.is_pasting = None; } keyboard::KeyCode::Tab | keyboard::KeyCode::Up @@ -748,15 +554,246 @@ where return event::Status::Captured; } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) - if self.state.is_focused => - { - self.state.keyboard_modifiers = modifiers; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + if state.is_focused { + state.keyboard_modifiers = modifiers; } - _ => {} } + _ => {} + } + + event::Status::Ignored +} - event::Status::Ignored +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +pub fn draw<Renderer>( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: &Value, + placeholder: &str, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + style_sheet: &dyn StyleSheet, +) where + Renderer: text::Renderer, +{ + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(&value); + + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background, + ); + + let text = value.to_string(); + let size = size.unwrap_or(renderer.default_size()); + + let (cursor, offset) = if state.is_focused() { + match state.cursor.state(&value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + position, + font.clone(), + ); + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.value_color(), + )), + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + left, + font.clone(), + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + right, + font.clone(), + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.selection_color(), + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { placeholder } else { &text }, + size, + font.clone(), + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } + + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color: if text.is_empty() { + style_sheet.placeholder_color() + } else { + style_sheet.value_color() + }, + font: font.clone(), + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render) + }); + } else { + render(renderer); + } +} + +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout(renderer, limits, self.width, self.padding, self.size) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + &self.font, + self.is_secure, + self.on_change.as_ref(), + &self.on_submit, + || &mut self.state, + ) } fn mouse_interaction( @@ -766,11 +803,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Text - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 48237edb..536aef78 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -1,5 +1,4 @@ //! Show toggle controls using togglers. - use crate::alignment; use crate::event; use crate::layout; @@ -14,7 +13,7 @@ use crate::{ pub use iced_style::toggler::{Style, StyleSheet}; -/// A toggler widget +/// A toggler widget. /// /// # Example /// @@ -32,7 +31,7 @@ pub use iced_style::toggler::{Style, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Toggler<'a, Message, Renderer: text::Renderer> { is_active: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: Option<String>, width: Length, size: u16, @@ -61,7 +60,7 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { f: F, ) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Toggler { is_active, diff --git a/pure/Cargo.toml b/pure/Cargo.toml new file mode 100644 index 00000000..317dccdf --- /dev/null +++ b/pure/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "iced_pure" +version = "0.1.0" +edition = "2021" + +[dependencies] +iced_native = { version = "0.4", path = "../native" } +iced_style = { version = "0.3", path = "../style" } +num-traits = "0.2" diff --git a/pure/src/element.rs b/pure/src/element.rs new file mode 100644 index 00000000..3d5697fe --- /dev/null +++ b/pure/src/element.rs @@ -0,0 +1,163 @@ +use crate::widget::tree::{self, Tree}; +use crate::widget::Widget; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub struct Element<'a, Message, Renderer> { + widget: Box<dyn Widget<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { + pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self { + Self { + widget: Box::new(widget), + } + } + + pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> { + self.widget.as_ref() + } + + pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> { + self.widget.as_mut() + } + + pub fn map<B>( + self, + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: iced_native::Renderer + 'a, + B: 'a, + { + Element::new(Map::new(self.widget, f)) + } +} + +struct Map<'a, A, B, Renderer> { + widget: Box<dyn Widget<A, Renderer> + 'a>, + mapper: Box<dyn Fn(A) -> B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new<F>( + widget: Box<dyn Widget<A, Renderer> + 'a>, + mapper: F, + ) -> Map<'a, A, B, Renderer> + where + F: 'a + Fn(A) -> B, + { + Map { + widget, + mapper: Box::new(mapper), + } + } +} + +impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> +where + Renderer: iced_native::Renderer + 'a, + A: 'a, + B: 'a, +{ + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec<Tree> { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + + fn width(&self) -> Length { + self.widget.width() + } + + fn height(&self) -> Length { + self.widget.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.widget.layout(renderer, limits) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, B>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let status = self.widget.on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + shell.merge(local_shell, &self.mapper); + + status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.widget.draw( + tree, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.widget.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + } +} diff --git a/pure/src/flex.rs b/pure/src/flex.rs new file mode 100644 index 00000000..8d473f08 --- /dev/null +++ b/pure/src/flex.rs @@ -0,0 +1,232 @@ +//! Distribute elements using a flex-based layout. +// This code is heavily inspired by the [`druid`] codebase. +// +// [`druid`]: https://github.com/xi-editor/druid +// +// Copyright 2018 The xi-editor Authors, Héctor Ramón +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::Element; + +use iced_native::layout::{Limits, Node}; +use iced_native::{Alignment, Padding, Point, Size}; + +/// The main axis of a flex layout. +#[derive(Debug)] +pub enum Axis { + /// The horizontal axis + Horizontal, + + /// The vertical axis + Vertical, +} + +impl Axis { + fn main(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.width, + Axis::Vertical => size.height, + } + } + + fn cross(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.height, + Axis::Vertical => size.width, + } + } + + fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + match self { + Axis::Horizontal => (main, cross), + Axis::Vertical => (cross, main), + } + } +} + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve<Message, Renderer>( + axis: Axis, + renderer: &Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[Element<Message, Renderer>], +) -> Node +where + Renderer: iced_native::Renderer, +{ + let limits = limits.pad(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + let max_cross = axis.cross(limits.max()); + + let mut fill_sum = 0; + let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut available = axis.main(limits.max()) - total_spacing; + + let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); + nodes.resize(items.len(), Node::default()); + + if align_items == Alignment::Fill { + let mut fill_cross = axis.cross(limits.min()); + + items.iter().for_each(|child| { + let cross_fill_factor = match axis { + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), + } + .fill_factor(); + + if cross_fill_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + fill_cross = fill_cross.max(axis.cross(size)); + } + }); + + cross = fill_cross; + } + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor == 0 { + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(0.0, cross) + } else { + axis.pack(0.0, 0.0) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(available, cross) + } else { + axis.pack(available, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(size)); + } + + nodes[i] = layout; + } else { + fill_sum += fill_factor; + } + } + + let remaining = available.max(0.0); + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor != 0 { + let max_main = remaining * fill_factor as f32 / fill_sum as f32; + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; + + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(min_main, cross) + } else { + axis.pack(min_main, axis.cross(limits.min())) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(max_main, cross) + } else { + axis.pack(max_main, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(layout.size())); + } + + nodes[i] = layout; + } + } + + let pad = axis.pack(padding.left as f32, padding.top as f32); + let mut main = pad.0; + + for (i, node) in nodes.iter_mut().enumerate() { + if i > 0 { + main += spacing; + } + + let (x, y) = axis.pack(main, pad.1); + + node.move_to(Point::new(x, y)); + + match axis { + Axis::Horizontal => { + node.align( + Alignment::Start, + align_items, + Size::new(0.0, cross), + ); + } + Axis::Vertical => { + node.align( + align_items, + Alignment::Start, + Size::new(cross, 0.0), + ); + } + } + + let size = node.size(); + + main += axis.main(size); + } + + let (width, height) = axis.pack(main - pad.0, cross); + let size = limits.resolve(Size::new(width, height)); + + Node::with_children(size.pad(padding), nodes) +} diff --git a/pure/src/helpers.rs b/pure/src/helpers.rs new file mode 100644 index 00000000..24f6dbaa --- /dev/null +++ b/pure/src/helpers.rs @@ -0,0 +1,153 @@ +use crate::widget; +use crate::Element; + +use iced_native::Length; +use std::borrow::Cow; +use std::ops::RangeInclusive; + +pub fn container<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + widget::Container::new(content) +} + +pub fn column<'a, Message, Renderer>() -> widget::Column<'a, Message, Renderer> +{ + widget::Column::new() +} + +pub fn row<'a, Message, Renderer>() -> widget::Row<'a, Message, Renderer> { + widget::Row::new() +} + +pub fn scrollable<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Scrollable<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + widget::Scrollable::new(content) +} + +pub fn button<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Button<'a, Message, Renderer> { + widget::Button::new(content) +} + +pub fn text<Renderer>(text: impl Into<String>) -> widget::Text<Renderer> +where + Renderer: iced_native::text::Renderer, +{ + widget::Text::new(text) +} + +pub fn checkbox<'a, Message, Renderer>( + label: impl Into<String>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Checkbox<'a, Message, Renderer> +where + Renderer: iced_native::text::Renderer, +{ + widget::Checkbox::new(is_checked, label, f) +} + +pub fn radio<'a, Message, Renderer, V>( + label: impl Into<String>, + value: V, + selected: Option<V>, + on_click: impl FnOnce(V) -> Message, +) -> widget::Radio<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, + V: Copy + Eq, +{ + widget::Radio::new(value, label, selected, on_click) +} + +pub fn toggler<'a, Message, Renderer>( + label: impl Into<Option<String>>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Toggler<'a, Message, Renderer> +where + Renderer: iced_native::text::Renderer, +{ + widget::Toggler::new(is_checked, label, f) +} + +pub fn text_input<'a, Message, Renderer>( + placeholder: &str, + value: &str, + on_change: impl Fn(String) -> Message + 'a, +) -> widget::TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, +{ + widget::TextInput::new(placeholder, value, on_change) +} + +pub fn slider<'a, Message, T>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::Slider<'a, T, Message> +where + Message: Clone, + T: Copy + From<u8> + std::cmp::PartialOrd, +{ + widget::Slider::new(range, value, on_change) +} + +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, +) -> widget::PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: iced_native::text::Renderer, +{ + widget::PickList::new(options, selected, on_selected) +} + +pub fn image<Handle>(handle: impl Into<Handle>) -> widget::Image<Handle> { + widget::Image::new(handle.into()) +} + +pub fn horizontal_space(width: Length) -> widget::Space { + widget::Space::with_width(width) +} + +pub fn vertical_space(height: Length) -> widget::Space { + widget::Space::with_height(height) +} + +/// Creates a horizontal [`Rule`] with the given height. +pub fn horizontal_rule<'a>(height: u16) -> widget::Rule<'a> { + widget::Rule::horizontal(height) +} + +/// Creates a vertical [`Rule`] with the given width. +pub fn vertical_rule<'a>(width: u16) -> widget::Rule<'a> { + widget::Rule::horizontal(width) +} + +/// Creates a new [`ProgressBar`]. +/// +/// It expects: +/// * an inclusive range of possible values +/// * the current value of the [`ProgressBar`] +pub fn progress_bar<'a>( + range: RangeInclusive<f32>, + value: f32, +) -> widget::ProgressBar<'a> { + widget::ProgressBar::new(range, value) +} diff --git a/pure/src/lib.rs b/pure/src/lib.rs new file mode 100644 index 00000000..ec2f29f8 --- /dev/null +++ b/pure/src/lib.rs @@ -0,0 +1,157 @@ +pub mod helpers; +pub mod overlay; +pub mod widget; + +pub(crate) mod flex; + +mod element; + +pub use element::Element; +pub use helpers::*; +pub use widget::Widget; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub struct Pure<'a, Message, Renderer> { + state: &'a mut State, + element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Pure<'a, Message, Renderer> +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + pub fn new( + state: &'a mut State, + content: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + let element = content.into(); + let _ = state.diff(&element); + + Self { state, element } + } +} + +pub struct State { + state_tree: widget::Tree, +} + +impl State { + pub fn new() -> Self { + Self { + state_tree: widget::Tree::empty(), + } + } + + fn diff<Message, Renderer>( + &mut self, + new_element: &Element<Message, Renderer>, + ) { + self.state_tree.diff(new_element); + } +} + +impl<'a, Message, Renderer> iced_native::Widget<Message, Renderer> + for Pure<'a, Message, Renderer> +where + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + fn width(&self) -> Length { + self.element.as_widget().width() + } + + fn height(&self) -> Length { + self.element.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.element.as_widget().layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.element.as_widget_mut().on_event( + &mut self.state.state_tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.element.as_widget().draw( + &self.state.state_tree, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.element.as_widget().mouse_interaction( + &self.state.state_tree, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.element.as_widget_mut().overlay( + &mut self.state.state_tree, + layout, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<iced_native::Element<'a, Message, Renderer>> + for Pure<'a, Message, Renderer> +where + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> iced_native::Element<'a, Message, Renderer> { + iced_native::Element::new(self) + } +} diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs new file mode 100644 index 00000000..c87dfce8 --- /dev/null +++ b/pure/src/overlay.rs @@ -0,0 +1,21 @@ +use crate::widget::Tree; + +use iced_native::Layout; + +pub use iced_native::overlay::*; + +pub fn from_children<'a, Message, Renderer>( + children: &'a [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option<Element<'a, Message, Renderer>> { + children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget().overlay(state, layout, renderer) + }) + .next() +} diff --git a/pure/src/widget.rs b/pure/src/widget.rs new file mode 100644 index 00000000..8200f9a7 --- /dev/null +++ b/pure/src/widget.rs @@ -0,0 +1,116 @@ +pub mod button; +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 rule; +pub mod scrollable; +pub mod slider; +pub mod svg; +pub mod text_input; +pub mod toggler; +pub mod tree; + +mod column; +mod row; +mod space; +mod text; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use column::Column; +pub use container::Container; +pub use image::Image; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use row::Row; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use space::Space; +pub use svg::Svg; +pub use text::Text; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tree::Tree; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub trait Widget<Message, Renderer> { + fn width(&self) -> Length; + + fn height(&self) -> Length; + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node; + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ); + + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + fn state(&self) -> tree::State { + tree::State::None + } + + fn children(&self) -> Vec<Tree> { + Vec::new() + } + + fn diff(&self, _tree: &mut Tree) {} + + fn mouse_interaction( + &self, + _state: &Tree, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + + fn on_event( + &mut self, + _state: &mut Tree, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + event::Status::Ignored + } + + fn overlay<'a>( + &'a self, + _state: &'a mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'a, Message, Renderer>> { + None + } +} diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs new file mode 100644 index 00000000..f99d3018 --- /dev/null +++ b/pure/src/widget/button.rs @@ -0,0 +1,225 @@ +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::button; +use iced_native::{ + Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +pub use iced_style::button::{Style, StyleSheet}; + +use button::State; + +pub struct Button<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + on_press: Option<Message>, + style_sheet: Box<dyn StyleSheet + 'a>, + width: Length, + height: Length, + padding: Padding, +} + +impl<'a, Message, Renderer> Button<'a, Message, Renderer> { + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + Button { + content: content.into(), + on_press: None, + style_sheet: Default::default(), + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5), + } + } + + /// Sets the width of the [`Button`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Button`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. + pub fn on_press(mut self, msg: Message) -> Self { + self.on_press = Some(msg); + self + } + + /// Sets the style of the [`Button`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Button<'a, Message, Renderer> +where + Message: 'static + Clone, + Renderer: 'static + iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + button::layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, &limits) + }, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + button::update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || tree.state.downcast_mut::<State>(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = button::draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + self.style_sheet.as_ref(), + || tree.state.downcast_ref::<State>(), + ); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + &renderer::Style { + text_color: styling.text_color, + }, + content_layout, + cursor_position, + &bounds, + ); + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + button::mouse_interaction( + layout, + cursor_position, + self.on_press.is_some(), + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Button<'a, Message, Renderer> +where + Message: Clone + 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs new file mode 100644 index 00000000..971980e3 --- /dev/null +++ b/pure/src/widget/checkbox.rs @@ -0,0 +1,103 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::checkbox::{Checkbox, Style, StyleSheet}; + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Checkbox<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs new file mode 100644 index 00000000..6b447270 --- /dev/null +++ b/pure/src/widget/column.rs @@ -0,0 +1,225 @@ +use crate::flex; +use crate::overlay; +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{ + Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, +}; + +use std::u32; + +pub struct Column<'a, Message, Renderer> { + spacing: u16, + padding: Padding, + width: Length, + height: Length, + max_width: u32, + align_items: Alignment, + children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Column<'a, Message, Renderer> { + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + pub fn with_children( + children: Vec<Element<'a, Message, Renderer>>, + ) -> Self { + Column { + spacing: 0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: u32::MAX, + align_items: Alignment::Start, + children, + } + } + + pub fn spacing(mut self, units: u16) -> Self { + self.spacing = units; + self + } + + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: u32) -> Self { + self.max_width = max_width; + self + } + + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Column<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); + + flex::resolve( + flex::Axis::Vertical, + renderer, + &limits, + self.padding, + self.spacing as f32, + self.align_items, + &self.children, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + style, + layout, + cursor_position, + viewport, + ); + } + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Column<'a, Message, Renderer> +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs new file mode 100644 index 00000000..91db1f3f --- /dev/null +++ b/pure/src/widget/container.rs @@ -0,0 +1,252 @@ +//! Decorate content and apply alignment. +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::alignment; +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::{ + Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::u32; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct Container<'a, Message, Renderer> { + padding: Padding, + width: Length, + height: Length, + max_width: u32, + max_height: u32, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + style_sheet: Box<dyn StyleSheet + 'a>, + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates an empty [`Container`]. + pub fn new<T>(content: T) -> Self + where + T: Into<Element<'a, Message, Renderer>>, + { + Container { + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: u32::MAX, + max_height: u32::MAX, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + style_sheet: Default::default(), + content: content.into(), + } + } + + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Container`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Container`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the maximum width of the [`Container`]. + pub fn max_width(mut self, max_width: u32) -> Self { + self.max_width = max_width; + self + } + + /// Sets the maximum height of the [`Container`] in pixels. + pub fn max_height(mut self, max_height: u32) -> Self { + self.max_height = max_height; + self + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { + self.vertical_alignment = alignment; + self + } + + /// Centers the contents in the horizontal axis of the [`Container`]. + pub fn center_x(mut self) -> Self { + self.horizontal_alignment = alignment::Horizontal::Center; + self + } + + /// Centers the contents in the vertical axis of the [`Container`]. + pub fn center_y(mut self) -> Self { + self.vertical_alignment = alignment::Vertical::Center; + self + } + + /// Sets the style of the [`Container`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + container::layout( + renderer, + limits, + self.width, + self.height, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let style = self.style_sheet.style(); + + container::draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + iced_native::Renderer, + Message: 'a, +{ + fn from( + column: Container<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs new file mode 100644 index 00000000..a5bca5a0 --- /dev/null +++ b/pure/src/widget/image.rs @@ -0,0 +1,66 @@ +use crate::widget::{Tree, Widget}; +use crate::Element; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::widget::image; +use iced_native::{Length, Point, Rectangle}; + +use std::hash::Hash; + +pub use image::Image; + +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> +where + Handle: Clone + Hash, + Renderer: iced_native::image::Renderer<Handle = Handle>, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, Renderer, Handle> Into<Element<'a, Message, Renderer>> + for Image<Handle> +where + Message: Clone + 'a, + Renderer: iced_native::image::Renderer<Handle = Handle> + 'a, + Handle: Clone + Hash + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/pane_grid.rs b/pure/src/widget/pane_grid.rs new file mode 100644 index 00000000..34a56bcc --- /dev/null +++ b/pure/src/widget/pane_grid.rs @@ -0,0 +1,400 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid +mod content; +mod title_bar; + +pub use content::Content; +pub use title_bar::TitleBar; + +pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split, + State, +}; + +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::pane_grid; +use iced_native::widget::pane_grid::state; +use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell}; + +pub use iced_style::pane_grid::{Line, StyleSheet}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/frailfreshairedaleterrier) +/// +/// This distribution of space is common in tiling window managers (like +/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +/// [`tmux`](https://github.com/tmux/tmux)). +/// +/// A [`PaneGrid`] supports: +/// +/// * Vertical and horizontal splits +/// * Tracking of the last active pane +/// * Mouse-based resizing +/// * Drag and drop to reorganize panes +/// * Hotkey support +/// * Configurable modifier keys +/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +/// +/// ## Example +/// +/// ``` +/// # use iced_pure::widget::pane_grid; +/// # use iced_pure::text; +/// # +/// # type PaneGrid<'a, Message> = +/// # iced_pure::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; +/// # +/// enum PaneState { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); +/// +/// let pane_grid = +/// PaneGrid::new(&state, |pane, state| { +/// pane_grid::Content::new(match state { +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized); +/// ``` +#[allow(missing_debug_implementations)] +pub struct PaneGrid<'a, Message, Renderer> { + state: &'a state::Internal, + elements: Vec<(Pane, Content<'a, Message, Renderer>)>, + width: Length, + height: Length, + spacing: u16, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a [`PaneGrid`] with the given [`State`] and view function. + /// + /// The view function will be called to display each [`Pane`] present in the + /// [`State`]. + pub fn new<T>( + state: &'a State<T>, + view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, + ) -> Self { + let elements = { + state + .panes + .iter() + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) + .collect() + }; + + Self { + elements, + state: &state.internal, + width: Length::Fill, + height: Length::Fill, + spacing: 0, + on_click: None, + on_drag: None, + on_resize: None, + style_sheet: Default::default(), + } + } + + /// Sets the width of the [`PaneGrid`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`PaneGrid`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the spacing _between_ the panes of the [`PaneGrid`]. + pub fn spacing(mut self, units: u16) -> Self { + self.spacing = units; + self + } + + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); + self + } + + /// Enables the drag and drop interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + pub fn on_drag<F>(mut self, f: F) -> Self + where + F: 'a + Fn(DragEvent) -> Message, + { + self.on_drag = Some(Box::new(f)); + self + } + + /// Enables the resize interactions of the [`PaneGrid`], which will + /// use the provided function to produce messages. + /// + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. + /// + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self + where + F: 'a + Fn(ResizeEvent) -> Message, + { + self.on_resize = Some((leeway, Box::new(f))); + self + } + + /// Sets the style of the [`PaneGrid`]. + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { + self.style_sheet = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<state::Action>() + } + + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) + } + + fn children(&self) -> Vec<Tree> { + self.elements + .iter() + .map(|(_, content)| content.state()) + .collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children_custom( + &self.elements, + |(_, content), state| content.diff(state), + |(_, content)| content.state(), + ) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + pane_grid::layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let action = tree.state.downcast_mut::<state::Action>(); + + let event_status = pane_grid::update( + action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|(((pane, content), tree), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + tree, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + pane_grid::mouse_interaction( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + }) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + pane_grid::draw( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + renderer, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style_sheet.as_ref(), + self.elements + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (*pane, (content, tree))), + |(content, tree), + renderer, + style, + layout, + cursor_position, + rectangle| { + content.draw( + tree, + renderer, + style, + layout, + cursor_position, + rectangle, + ); + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|(((_, pane), tree), layout)| { + pane.overlay(tree, layout, renderer) + }) + .next() + } +} + +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + iced_native::Renderer, + Message: 'a, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} diff --git a/pure/src/widget/pane_grid/content.rs b/pure/src/widget/pane_grid/content.rs new file mode 100644 index 00000000..a928b28c --- /dev/null +++ b/pure/src/widget/pane_grid/content.rs @@ -0,0 +1,331 @@ +use crate::widget::pane_grid::TitleBar; +use crate::widget::tree::Tree; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::widget::pane_grid::Draggable; +use iced_native::{Clipboard, Layout, Point, Rectangle, Shell, Size}; + +/// The content of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer> { + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style_sheet: Box<dyn container::StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style_sheet: Default::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + pub fn state(&self) -> Tree { + let children = if let Some(title_bar) = self.title_bar.as_ref() { + vec![Tree::new(&self.body), title_bar.state()] + } else { + vec![Tree::new(&self.body), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(title_bar) = self.title_bar.as_ref() { + title_bar.diff(&mut tree.children[1]); + } + + tree.children[0].diff(&self.body); + } else { + *tree = self.state(); + } + } + + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + { + let style = self.style_sheet.style(); + + container::draw_background(renderer, &style, bounds); + } + + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + let show_controls = bounds.contains(cursor_position); + + title_bar.draw( + &tree.children[1], + renderer, + style, + title_bar_layout, + cursor_position, + viewport, + show_controls, + ); + + self.body.as_widget().draw( + &tree.children[0], + renderer, + style, + body_layout, + cursor_position, + viewport, + ); + } else { + self.body.as_widget().draw( + &tree.children[0], + renderer, + style, + layout, + cursor_position, + viewport, + ); + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.as_widget().layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.as_widget().layout(renderer, limits) + } + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + is_picked: bool, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + &mut tree.children[1], + event.clone(), + children.next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.as_widget_mut().on_event( + &mut tree.children[0], + event, + body_layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }; + + event_status.merge(body_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let (body_layout, title_bar_interaction) = + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = title_bar + .is_over_pick_area(title_bar_layout, cursor_position); + + if is_over_pick_area { + return mouse::Interaction::Grab; + } + + let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], + title_bar_layout, + cursor_position, + viewport, + renderer, + ); + + (children.next().unwrap(), mouse_interaction) + } else { + (layout, mouse::Interaction::default()) + }; + + self.body + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + viewport, + renderer, + ) + .max(title_bar_interaction) + } + + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + if let Some(title_bar) = self.title_bar.as_ref() { + let mut children = layout.children(); + let title_bar_layout = children.next()?; + + let mut states = tree.children.iter_mut(); + let body_state = states.next().unwrap(); + let title_bar_state = states.next().unwrap(); + + match title_bar.overlay(title_bar_state, title_bar_layout, renderer) + { + Some(overlay) => Some(overlay), + None => self.body.as_widget().overlay( + body_state, + children.next()?, + renderer, + ), + } + } else { + self.body.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } + } +} + +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: iced_native::Renderer, +{ + fn from(element: T) -> Self { + Self::new(element) + } +} diff --git a/pure/src/widget/pane_grid/title_bar.rs b/pure/src/widget/pane_grid/title_bar.rs new file mode 100644 index 00000000..dd68b073 --- /dev/null +++ b/pure/src/widget/pane_grid/title_bar.rs @@ -0,0 +1,355 @@ +use crate::widget::Tree; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::{Clipboard, Layout, Padding, Point, Rectangle, Shell, Size}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + controls: Option<Element<'a, Message, Renderer>>, + padding: Padding, + always_show_controls: bool, + style_sheet: Box<dyn container::StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`TitleBar`] with the given content. + pub fn new<E>(content: E) -> Self + where + E: Into<Element<'a, Message, Renderer>>, + { + Self { + content: content.into(), + controls: None, + padding: Padding::ZERO, + always_show_controls: false, + style_sheet: Default::default(), + } + } + + /// Sets the controls of the [`TitleBar`]. + pub fn controls( + mut self, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style( + mut self, + style: impl Into<Box<dyn container::StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + pub fn state(&self) -> Tree { + let children = if let Some(controls) = self.controls.as_ref() { + vec![Tree::new(&self.content), Tree::new(controls)] + } else { + vec![Tree::new(&self.content), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(controls) = self.controls.as_ref() { + tree.children[1].diff(controls); + } + + tree.children[0].diff(&self.content); + } else { + *tree = self.state(); + } + } + + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + show_controls: bool, + ) { + let bounds = layout.bounds(); + let style = self.style_sheet.style(); + let inherited_style = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + container::draw_background(renderer, &style, bounds); + + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + &inherited_style, + title_layout, + cursor_position, + viewport, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + if show_controls || self.always_show_controls { + controls.as_widget().draw( + &tree.children[1], + renderer, + &inherited_style, + controls_layout, + cursor_position, + viewport, + ); + } + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + if self.controls.is_some() { + let controls_layout = children.next().unwrap(); + + !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } else { + !title_layout.bounds().contains(cursor_position) + } + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.pad(self.padding); + let max_size = limits.max(); + + let title_layout = self + .content + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_size = title_layout.size(); + + let mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + controls_layout.move_to(Point::new(space_before_controls, 0.0)); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::with_children( + Size::new(max_size.width, title_size.height), + vec![title_layout], + ) + }; + + node.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); + + layout::Node::with_children(node.size().pad(self.padding), vec![node]) + } + + pub(crate) fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let control_status = if let Some(controls) = &mut self.controls { + let controls_layout = children.next().unwrap(); + + controls.as_widget_mut().on_event( + &mut tree.children[1], + event.clone(), + controls_layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; + + let title_status = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + title_layout, + cursor_position, + renderer, + clipboard, + shell, + ); + + control_status.merge(title_status) + } + + pub(crate) fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], + title_layout, + cursor_position, + viewport, + renderer, + ); + + if let Some(controls) = &self.controls { + let controls_layout = children.next().unwrap(); + + controls + .as_widget() + .mouse_interaction( + &tree.children[1], + controls_layout, + cursor_position, + viewport, + renderer, + ) + .max(title_interaction) + } else { + title_interaction + } + } + + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + let mut states = tree.children.iter_mut(); + let title_state = states.next().unwrap(); + let controls_state = states.next().unwrap(); + + content + .as_widget() + .overlay(title_state, title_layout, renderer) + .or_else(move || { + controls.as_ref().and_then(|controls| { + let controls_layout = children.next()?; + + controls.as_widget().overlay( + controls_state, + controls_layout, + renderer, + ) + }) + }) + } +} diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs new file mode 100644 index 00000000..9573f27a --- /dev/null +++ b/pure/src/widget/pick_list.rs @@ -0,0 +1,234 @@ +//! Display a dropdown list of selectable values. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::pick_list; +use iced_native::{ + Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::borrow::Cow; + +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: text::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + on_selected: Box<dyn Fn(T) -> Message + 'a>, + options: Cow<'a, [T]>, + placeholder: Option<String>, + selected: Option<T>, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T: 'a, Message, Renderer: text::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// The default padding of a [`PickList`]. + pub const DEFAULT_PADDING: Padding = Padding::new(5); + + /// Creates a new [`PickList`] with the given [`State`], a list of options, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, + ) -> Self { + Self { + on_selected: Box::new(on_selected), + options: options.into(), + placeholder: None, + selected, + width: Length::Shrink, + text_size: None, + padding: Self::DEFAULT_PADDING, + font: Default::default(), + style_sheet: Default::default(), + } + } + + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + + /// Sets the width of the [`PickList`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<pick_list::State<T>>() + } + + fn state(&self) -> tree::State { + tree::State::new(pick_list::State::<T>::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + pick_list::layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + pick_list::update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<pick_list::State<T>>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + pick_list::mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + pick_list::draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_mut::<pick_list::State<T>>(); + + pick_list::overlay( + layout, + state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) + } +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/progress_bar.rs b/pure/src/widget/progress_bar.rs new file mode 100644 index 00000000..3f4ffd55 --- /dev/null +++ b/pure/src/widget/progress_bar.rs @@ -0,0 +1,100 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::progress_bar::*; + +impl<'a, Message, Renderer> Widget<Message, Renderer> for ProgressBar<'a> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for ProgressBar<'a> +where + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs new file mode 100644 index 00000000..c20f8f3e --- /dev/null +++ b/pure/src/widget/radio.rs @@ -0,0 +1,104 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::radio::{Radio, Style, StyleSheet}; + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Radio<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Radio<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs new file mode 100644 index 00000000..d7f90540 --- /dev/null +++ b/pure/src/widget/row.rs @@ -0,0 +1,212 @@ +use crate::flex; +use crate::overlay; +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{ + Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, +}; + +pub struct Row<'a, Message, Renderer> { + spacing: u16, + padding: Padding, + width: Length, + height: Length, + align_items: Alignment, + children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Message, Renderer> Row<'a, Message, Renderer> { + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + pub fn with_children( + children: Vec<Element<'a, Message, Renderer>>, + ) -> Self { + Row { + spacing: 0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align_items: Alignment::Start, + children, + } + } + + pub fn spacing(mut self, units: u16) -> Self { + self.spacing = units; + self + } + + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Row<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + flex::resolve( + flex::Axis::Horizontal, + renderer, + &limits, + self.padding, + self.spacing as f32, + self.align_items, + &self.children, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + style, + layout, + cursor_position, + viewport, + ); + } + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Row<'a, Message, Renderer> +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/rule.rs b/pure/src/widget/rule.rs new file mode 100644 index 00000000..40b1fc90 --- /dev/null +++ b/pure/src/widget/rule.rs @@ -0,0 +1,99 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::rule::*; + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Rule<'a> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Rule<'a> +where + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs new file mode 100644 index 00000000..f9a51200 --- /dev/null +++ b/pure/src/widget/scrollable.rs @@ -0,0 +1,265 @@ +use crate::overlay; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::scrollable; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Vector}; + +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// A widget that can vertically display an infinite amount of content with a +/// scrollbar. +#[allow(missing_debug_implementations)] +pub struct Scrollable<'a, Message, Renderer> { + height: Length, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + on_scroll: Option<Box<dyn Fn(f32) -> Message>>, + style_sheet: Box<dyn StyleSheet + 'a>, + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer: iced_native::Renderer> + Scrollable<'a, Message, Renderer> +{ + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + Scrollable { + height: Length::Shrink, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, + on_scroll: None, + style_sheet: Default::default(), + content: content.into(), + } + } + + /// Sets the height of the [`Scrollable`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// + /// It silently enforces a minimum value of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } + + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative offset of the [`Scrollable`] + /// (e.g. `0` means top, while `1` means bottom). + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + + /// Sets the style of the [`Scrollable`] . + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<scrollable::State>() + } + + fn state(&self) -> tree::State { + tree::State::new(scrollable::State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + scrollable::layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + scrollable::update( + tree.state.downcast_mut::<scrollable::State>(), + event, + layout, + cursor_position, + clipboard, + shell, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + scrollable::draw( + tree.state.downcast_ref::<scrollable::State>(), + renderer, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style_sheet.as_ref(), + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + scrollable::mouse_interaction( + tree.state.downcast_ref::<scrollable::State>(), + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content + .as_widget() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = tree + .state + .downcast_ref::<scrollable::State>() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } +} + +impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_native::Renderer, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs new file mode 100644 index 00000000..1107bdc1 --- /dev/null +++ b/pure/src/widget/slider.rs @@ -0,0 +1,243 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::slider; +use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell, Size}; + +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`Slider`] will try to fill the horizontal space of its container. +/// +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ``` +/// # use iced_native::widget::slider::{self, Slider}; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let state = &mut slider::State::new(); +/// let value = 50.0; +/// +/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct Slider<'a, T, Message> { + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message + 'a>, + on_release: Option<Message>, + width: Length, + height: u16, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, +{ + /// The default height of a [`Slider`]. + pub const DEFAULT_HEIGHT: u16 = 22; + + /// Creates a new [`Slider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`Slider`] + /// * a function that will be called when the [`Slider`] is dragged. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + + Slider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Length::Fill, + height: Self::DEFAULT_HEIGHT, + style_sheet: Default::default(), + } + } + + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + + /// Sets the style of the [`Slider`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message> +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<slider::State>() + } + + fn state(&self) -> tree::State { + tree::State::new(slider::State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(self.width).height(Length::Units(self.height)); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + slider::update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::<slider::State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + slider::draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<slider::State>(), + self.value, + &self.range, + self.style_sheet.as_ref(), + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + slider::mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::<slider::State>(), + ) + } +} + +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + iced_native::Renderer, +{ + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs new file mode 100644 index 00000000..b408153b --- /dev/null +++ b/pure/src/widget/space.rs @@ -0,0 +1,99 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::Space; + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Space +where + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/svg.rs b/pure/src/widget/svg.rs new file mode 100644 index 00000000..2758c5b1 --- /dev/null +++ b/pure/src/widget/svg.rs @@ -0,0 +1,62 @@ +use crate::widget::{Tree, Widget}; +use crate::Element; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::widget::svg; +use iced_native::{Length, Point, Rectangle}; + +pub use iced_native::svg::Handle; +pub use svg::Svg; + +impl<Message, Renderer> Widget<Message, Renderer> for Svg +where + Renderer: iced_native::svg::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Svg +where + Message: Clone + 'a, + Renderer: iced_native::svg::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs new file mode 100644 index 00000000..b78d4117 --- /dev/null +++ b/pure/src/widget/text.rs @@ -0,0 +1,70 @@ +use crate::widget::Tree; +use crate::{Element, Widget}; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Length, Point, Rectangle}; + +pub use iced_native::widget::Text; + +impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> +where + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Text<Renderer> +where + Renderer: text::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for &'a str +where + Renderer: text::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Text::new(self).into() + } +} diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs new file mode 100644 index 00000000..d6041d7f --- /dev/null +++ b/pure/src/widget/text_input.rs @@ -0,0 +1,241 @@ +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::text_input; +use iced_native::{Clipboard, Length, Padding, Point, Rectangle, Shell}; + +pub use iced_style::text_input::{Style, StyleSheet}; + +/// A field that can be filled with text. +/// +/// # Example +/// ``` +/// # use iced_native::renderer::Null; +/// # use iced_native::widget::text_input; +/// # +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; +/// #[derive(Debug, Clone)] +/// enum Message { +/// TextInputChanged(String), +/// } +/// +/// let mut state = text_input::State::new(); +/// let value = "Some text"; +/// +/// let input = TextInput::new( +/// &mut state, +/// "This is the placeholder...", +/// value, +/// Message::TextInputChanged, +/// ) +/// .padding(10); +/// ``` +///  +#[allow(missing_debug_implementations)] +pub struct TextInput<'a, Message, Renderer: text::Renderer> { + placeholder: String, + value: text_input::Value, + is_secure: bool, + font: Renderer::Font, + width: Length, + padding: Padding, + size: Option<u16>, + on_change: Box<dyn Fn(String) -> Message + 'a>, + on_submit: Option<Message>, + style_sheet: Box<dyn StyleSheet + 'a>, +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, +{ + /// Creates a new [`TextInput`]. + /// + /// It expects: + /// - some [`State`] + /// - a placeholder + /// - the current value + /// - a function that produces a message when the [`TextInput`] changes + pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + TextInput { + placeholder: String::from(placeholder), + value: text_input::Value::new(value), + is_secure: false, + font: Default::default(), + width: Length::Fill, + padding: Padding::ZERO, + size: None, + on_change: Box::new(on_change), + on_submit: None, + style_sheet: Default::default(), + } + } + + /// Converts the [`TextInput`] into a secure password input. + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + + /// Sets the [`Font`] of the [`Text`]. + /// + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + /// Sets the width of the [`TextInput`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the text size of the [`TextInput`]. + pub fn size(mut self, size: u16) -> Self { + self.size = Some(size); + self + } + + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed. + pub fn on_submit(mut self, message: Message) -> Self { + self.on_submit = Some(message); + self + } + + /// Sets the style of the [`TextInput`]. + pub fn style( + mut self, + style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<text_input::State>() + } + + fn state(&self) -> tree::State { + tree::State::new(text_input::State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + text_input::layout( + renderer, + limits, + self.width, + self.padding, + self.size, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + text_input::update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + &self.font, + self.is_secure, + self.on_change.as_ref(), + &self.on_submit, + || tree.state.downcast_mut::<text_input::State>(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + text_input::draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<text_input::State>(), + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style_sheet.as_ref(), + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + text_input::mouse_interaction(layout, cursor_position) + } +} + +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, +{ + fn from( + text_input: TextInput<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} diff --git a/pure/src/widget/toggler.rs b/pure/src/widget/toggler.rs new file mode 100644 index 00000000..1b3367a4 --- /dev/null +++ b/pure/src/widget/toggler.rs @@ -0,0 +1,103 @@ +use crate::widget::{Tree, Widget}; +use crate::Element; + +use iced_native::event::{self, Event}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; + +pub use iced_native::widget::toggler::{Style, StyleSheet, Toggler}; + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::width(self) + } + + fn height(&self) -> Length { + <Self as iced_native::Widget<Message, Renderer>>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + <Self as iced_native::Widget<Message, Renderer>>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + <Self as iced_native::Widget<Message, Renderer>>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + <Self as iced_native::Widget<Message, Renderer>>::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn on_event( + &mut self, + _state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + <Self as iced_native::Widget<Message, Renderer>>::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } +} + +impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for Toggler<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs new file mode 100644 index 00000000..bd7c259c --- /dev/null +++ b/pure/src/widget/tree.rs @@ -0,0 +1,128 @@ +use crate::Element; + +use std::any::{self, Any}; + +pub struct Tree { + pub tag: Tag, + pub state: State, + pub children: Vec<Tree>, +} + +impl Tree { + pub fn empty() -> Self { + Self { + tag: Tag::stateless(), + state: State::None, + children: Vec::new(), + } + } + + pub fn new<Message, Renderer>( + element: &Element<'_, Message, Renderer>, + ) -> Self { + Self { + tag: element.as_widget().tag(), + state: element.as_widget().state(), + children: element.as_widget().children(), + } + } + + pub fn diff<Message, Renderer>( + &mut self, + new: &Element<'_, Message, Renderer>, + ) { + if self.tag == new.as_widget().tag() { + new.as_widget().diff(self) + } else { + *self = Self::new(new); + } + } + + pub fn diff_children<Message, Renderer>( + &mut self, + new_children: &[Element<'_, Message, Renderer>], + ) { + self.diff_children_custom( + new_children, + |new, child_state| child_state.diff(new), + Self::new, + ) + } + + pub fn diff_children_custom<T>( + &mut self, + new_children: &[T], + diff: impl Fn(&T, &mut Tree), + new_state: impl Fn(&T) -> Self, + ) { + if self.children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + diff(new, child_state); + } + + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(new_state), + ); + } + } +} + +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + pub fn of<T>() -> Self + where + T: 'static, + { + Self(any::TypeId::of::<T>()) + } + + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +pub enum State { + None, + Some(Box<dyn Any>), +} + +impl State { + pub fn new<T>(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + + pub fn downcast_ref<T>(&self) -> &T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } + } + + pub fn downcast_mut<T>(&mut self) -> &mut T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } + } +} @@ -195,6 +195,9 @@ pub mod time; pub mod widget; pub mod window; +#[cfg(feature = "pure")] +pub mod pure; + #[cfg(all(not(feature = "glow"), feature = "wgpu"))] use iced_winit as runtime; @@ -214,6 +217,7 @@ pub use application::Application; pub use element::Element; pub use error::Error; pub use executor::Executor; +pub use renderer::Renderer; pub use result::Result; pub use sandbox::Sandbox; pub use settings::Settings; diff --git a/src/pure.rs b/src/pure.rs new file mode 100644 index 00000000..948183f1 --- /dev/null +++ b/src/pure.rs @@ -0,0 +1,33 @@ +//! Leverage pure, virtual widgets in your application. +//! +//! The widgets found in this module are completely stateless versions of +//! [the original widgets]. +//! +//! Effectively, this means that, as a user of the library, you do not need to +//! keep track of the local state of each widget (e.g. [`button::State`]). +//! Instead, the runtime will keep track of everything for you! +//! +//! You can embed pure widgets anywhere in your [impure `Application`] using the +//! [`Pure`] widget and some [`State`]. +//! +//! In case you want to only use pure widgets in your application, this module +//! offers an alternate [`Application`] trait with a completely pure `view` +//! method. +//! +//! [the original widgets]: crate::widget +//! [`button::State`]: crate::widget::button::State +//! [impure `Application`]: crate::Application +pub mod widget; + +mod application; +mod sandbox; + +pub use application::Application; +pub use sandbox::Sandbox; + +pub use iced_pure::helpers::*; +pub use iced_pure::{Pure, State}; + +/// A generic, pure [`Widget`]. +pub type Element<'a, Message> = + iced_pure::Element<'a, Message, crate::Renderer>; diff --git a/src/pure/application.rs b/src/pure/application.rs new file mode 100644 index 00000000..5f400bea --- /dev/null +++ b/src/pure/application.rs @@ -0,0 +1,186 @@ +use crate::pure::{self, Pure}; +use crate::window; +use crate::{Color, Command, Executor, Settings, Subscription}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Application`]: crate::Application +/// [`Element`]: pure::Element +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> pure::Element<'_, Self::Message>; + + /// Returns the current [`Application`] mode. + /// + /// The runtime will automatically transition your application if a new mode + /// is returned. + /// + /// Currently, the mode only has an effect in native platforms. + /// + /// By default, an application will run in windowed mode. + fn mode(&self) -> window::Mode { + window::Mode::Windowed + } + + /// Returns the background color of the [`Application`]. + /// + /// By default, it returns [`Color::WHITE`]. + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// Runs the [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings<Self::Flags>) -> crate::Result + where + Self: 'static, + { + <Instance<Self> as crate::Application>::run(settings) + } +} + +struct Instance<A: Application> { + application: A, + state: pure::State, +} + +impl<A> crate::Application for Instance<A> +where + A: Application, + A::Message: 'static, +{ + type Executor = A::Executor; + type Message = A::Message; + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) { + let (application, command) = A::new(flags); + + ( + Instance { + application, + state: pure::State::new(), + }, + command, + ) + } + + fn title(&self) -> String { + A::title(&self.application) + } + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + A::update(&mut self.application, message) + } + + fn subscription(&self) -> Subscription<Self::Message> { + A::subscription(&self.application) + } + + fn view(&mut self) -> crate::Element<'_, Self::Message> { + let content = A::view(&self.application); + + Pure::new(&mut self.state, content).into() + } + + fn mode(&self) -> window::Mode { + A::mode(&self.application) + } + + fn background_color(&self) -> Color { + A::background_color(&self.application) + } + + fn scale_factor(&self) -> f64 { + A::scale_factor(&self.application) + } + + fn should_exit(&self) -> bool { + A::should_exit(&self.application) + } +} diff --git a/src/pure/sandbox.rs b/src/pure/sandbox.rs new file mode 100644 index 00000000..fbd1d7a8 --- /dev/null +++ b/src/pure/sandbox.rs @@ -0,0 +1,119 @@ +use crate::pure; +use crate::{Color, Command, Error, Settings, Subscription}; + +/// A pure version of [`Sandbox`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Sandbox`]: crate::Sandbox +/// [`Element`]: pure::Element +pub trait Sandbox { + /// The type of __messages__ your [`Sandbox`] will produce. + type Message: std::fmt::Debug + Send; + + /// Initializes the [`Sandbox`]. + /// + /// Here is where you should return the initial state of your app. + fn new() -> Self; + + /// Returns the current title of the [`Sandbox`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> String; + + /// Handles a __message__ and updates the state of the [`Sandbox`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by user interactions, will be handled by this method. + fn update(&mut self, message: Self::Message); + + /// Returns the widgets to display in the [`Sandbox`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> pure::Element<'_, Self::Message>; + + /// Returns the background color of the [`Sandbox`]. + /// + /// By default, it returns [`Color::WHITE`]. + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Sandbox`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// Runs the [`Sandbox`]. + /// + /// On native platforms, this method will take control of the current thread + /// and __will NOT return__. + /// + /// It should probably be that last thing you call in your `main` function. + fn run(settings: Settings<()>) -> Result<(), Error> + where + Self: 'static + Sized, + { + <Self as pure::Application>::run(settings) + } +} + +impl<T> pure::Application for T +where + T: Sandbox, +{ + type Executor = iced_futures::backend::null::Executor; + type Flags = (); + type Message = T::Message; + + fn new(_flags: ()) -> (Self, Command<T::Message>) { + (T::new(), Command::none()) + } + + fn title(&self) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command<T::Message> { + T::update(self, message); + + Command::none() + } + + fn subscription(&self) -> Subscription<T::Message> { + Subscription::none() + } + + fn view(&self) -> pure::Element<'_, T::Message> { + T::view(self) + } + + fn background_color(&self) -> Color { + T::background_color(self) + } + + fn scale_factor(&self) -> f64 { + T::scale_factor(self) + } + + fn should_exit(&self) -> bool { + T::should_exit(self) + } +} diff --git a/src/pure/widget.rs b/src/pure/widget.rs new file mode 100644 index 00000000..6628b1fb --- /dev/null +++ b/src/pure/widget.rs @@ -0,0 +1,167 @@ +//! Pure versions of the widgets. + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = + iced_pure::widget::Column<'a, Message, crate::Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = + iced_pure::widget::Row<'a, Message, crate::Renderer>; + +/// A paragraph of text. +pub type Text = iced_pure::widget::Text<crate::Renderer>; + +pub mod button { + //! Allow your users to perform actions by pressing a button. + pub use iced_pure::widget::button::{Style, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Button<'a, Message> = + iced_pure::widget::Button<'a, Message, crate::Renderer>; +} + +pub mod checkbox { + //! Show toggle controls using checkboxes. + pub use iced_pure::widget::checkbox::{Style, StyleSheet}; + + /// A box that can be checked. + pub type Checkbox<'a, Message> = + iced_native::widget::Checkbox<'a, Message, crate::Renderer>; +} + +pub mod container { + //! Decorate content and apply alignment. + pub use iced_pure::widget::container::{Style, StyleSheet}; + + /// An element decorating some content. + pub type Container<'a, Message> = + iced_pure::widget::Container<'a, Message, crate::Renderer>; +} + +pub mod pane_grid { + //! Let your users split regions of your application and organize layout dynamically. + //! + //! [](https://gfycat.com/mixedflatjellyfish) + //! + //! # Example + //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, + //! drag and drop, and hotkey support. + //! + //! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid + pub use iced_pure::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Line, Node, Pane, + ResizeEvent, Split, State, StyleSheet, + }; + + /// A collection of panes distributed using either vertical or horizontal splits + /// to completely fill the space available. + /// + /// [](https://gfycat.com/mixedflatjellyfish) + pub type PaneGrid<'a, Message> = + iced_pure::widget::PaneGrid<'a, Message, crate::Renderer>; + + /// The content of a [`Pane`]. + pub type Content<'a, Message> = + iced_pure::widget::pane_grid::Content<'a, Message, crate::Renderer>; + + /// The title bar of a [`Pane`]. + pub type TitleBar<'a, Message> = + iced_pure::widget::pane_grid::TitleBar<'a, Message, crate::Renderer>; +} + +pub mod pick_list { + //! Display a dropdown list of selectable values. + pub use iced_pure::overlay::menu::Style as Menu; + pub use iced_pure::widget::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_pure::widget::PickList<'a, T, Message, crate::Renderer>; +} + +pub mod radio { + //! Create choices using radio buttons. + pub use iced_pure::widget::radio::{Style, StyleSheet}; + + /// A circular button representing a choice. + pub type Radio<'a, Message> = + iced_pure::widget::Radio<'a, Message, crate::Renderer>; +} + +pub mod scrollable { + //! Navigate an endless amount of content with a scrollbar. + pub use iced_pure::widget::scrollable::{Scrollbar, Scroller, StyleSheet}; + + /// A widget that can vertically display an infinite amount of content + /// with a scrollbar. + pub type Scrollable<'a, Message> = + iced_pure::widget::Scrollable<'a, Message, crate::Renderer>; +} + +pub mod toggler { + //! Show toggle controls using togglers. + pub use iced_pure::widget::toggler::{Style, StyleSheet}; + + /// A toggler widget. + pub type Toggler<'a, Message> = + iced_pure::widget::Toggler<'a, Message, crate::Renderer>; +} + +pub mod text_input { + //! Display fields that can be filled with text. + use crate::Renderer; + + pub use iced_pure::widget::text_input::{Style, StyleSheet}; + + /// A field that can be filled with text. + pub type TextInput<'a, Message> = + iced_pure::widget::TextInput<'a, Message, Renderer>; +} + +pub use iced_pure::widget::progress_bar; +pub use iced_pure::widget::rule; +pub use iced_pure::widget::slider; +pub use iced_pure::widget::Space; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use container::Container; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use text_input::TextInput; +pub use toggler::Toggler; + +#[cfg(feature = "canvas")] +pub use iced_graphics::widget::pure::canvas; + +#[cfg(feature = "qr_code")] +pub use iced_graphics::widget::pure::qr_code; + +#[cfg(feature = "image")] +pub mod image { + //! Display images in your user interface. + pub use iced_native::image::Handle; + + /// A frame that displays an image. + pub type Image = iced_pure::widget::Image<Handle>; +} + +#[cfg(feature = "svg")] +pub use iced_pure::widget::svg; + +#[cfg(feature = "canvas")] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; + +#[cfg(feature = "image")] +pub use image::Image; + +#[cfg(feature = "svg")] +pub use svg::Svg; diff --git a/src/widget.rs b/src/widget.rs index c619bcfa..9cc0832f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -13,53 +13,192 @@ //! //! These widgets have their own module with a `State` type. For instance, a //! [`TextInput`] has some [`text_input::State`]. -pub use crate::renderer::widget::{ - button, checkbox, container, pane_grid, pick_list, progress_bar, radio, - rule, scrollable, slider, text_input, toggler, tooltip, Column, Row, Space, - Text, -}; - -#[cfg(any(feature = "canvas", feature = "glow_canvas"))] -#[cfg_attr( - docsrs, - doc(cfg(any(feature = "canvas", feature = "glow_canvas"))) -)] -pub use crate::renderer::widget::canvas; - -#[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] -#[cfg_attr( - docsrs, - doc(cfg(any(feature = "qr_code", feature = "glow_qr_code"))) -)] -pub use crate::renderer::widget::qr_code; - -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = + iced_native::widget::Column<'a, Message, crate::Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = + iced_native::widget::Row<'a, Message, crate::Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::widget::Text<crate::Renderer>; + +pub mod button { + //! Allow your users to perform actions by pressing a button. + //! + //! A [`Button`] has some local [`State`]. + pub use iced_native::widget::button::{State, Style, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Button<'a, Message> = + iced_native::widget::Button<'a, Message, crate::Renderer>; +} + +pub mod checkbox { + //! Show toggle controls using checkboxes. + pub use iced_native::widget::checkbox::{Style, StyleSheet}; + + /// A box that can be checked. + pub type Checkbox<'a, Message> = + iced_native::widget::Checkbox<'a, Message, crate::Renderer>; +} + +pub mod container { + //! Decorate content and apply alignment. + pub use iced_native::widget::container::{Style, StyleSheet}; + + /// An element decorating some content. + pub type Container<'a, Message> = + iced_native::widget::Container<'a, Message, crate::Renderer>; +} + +pub mod pane_grid { + //! Let your users split regions of your application and organize layout dynamically. + //! + //! [](https://gfycat.com/mixedflatjellyfish) + //! + //! # Example + //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, + //! drag and drop, and hotkey support. + //! + //! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid + pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Line, Node, Pane, + ResizeEvent, Split, State, StyleSheet, + }; + + /// A collection of panes distributed using either vertical or horizontal splits + /// to completely fill the space available. + /// + /// [](https://gfycat.com/mixedflatjellyfish) + pub type PaneGrid<'a, Message> = + iced_native::widget::PaneGrid<'a, Message, crate::Renderer>; + + /// The content of a [`Pane`]. + pub type Content<'a, Message> = + iced_native::widget::pane_grid::Content<'a, Message, crate::Renderer>; + + /// The title bar of a [`Pane`]. + pub type TitleBar<'a, Message> = + iced_native::widget::pane_grid::TitleBar<'a, Message, crate::Renderer>; +} + +pub mod pick_list { + //! Display a dropdown list of selectable values. + pub use iced_native::overlay::menu::Style as Menu; + pub use iced_native::widget::pick_list::{State, Style, StyleSheet}; + + /// A widget allowing the selection of a single value from a list of options. + pub type PickList<'a, T, Message> = + iced_native::widget::PickList<'a, T, Message, crate::Renderer>; +} + +pub mod radio { + //! Create choices using radio buttons. + pub use iced_native::widget::radio::{Style, StyleSheet}; + + /// A circular button representing a choice. + pub type Radio<'a, Message> = + iced_native::widget::Radio<'a, Message, crate::Renderer>; +} + +pub mod scrollable { + //! Navigate an endless amount of content with a scrollbar. + pub use iced_native::widget::scrollable::{ + style::Scrollbar, style::Scroller, State, StyleSheet, + }; + + /// A widget that can vertically display an infinite amount of content + /// with a scrollbar. + pub type Scrollable<'a, Message> = + iced_native::widget::Scrollable<'a, Message, crate::Renderer>; +} + +pub mod toggler { + //! Show toggle controls using togglers. + pub use iced_native::widget::toggler::{Style, StyleSheet}; + + /// A toggler widget. + pub type Toggler<'a, Message> = + iced_native::widget::Toggler<'a, Message, crate::Renderer>; +} + +pub mod text_input { + //! Display fields that can be filled with text. + //! + //! A [`TextInput`] has some local [`State`]. + use crate::Renderer; + + pub use iced_native::widget::text_input::{State, Style, StyleSheet}; + + /// A field that can be filled with text. + pub type TextInput<'a, Message> = + iced_native::widget::TextInput<'a, Message, Renderer>; +} + +pub mod tooltip { + //! Display a widget over another. + pub use iced_native::widget::tooltip::Position; + + /// A widget allowing the selection of a single value from a list of options. + pub type Tooltip<'a, Message> = + iced_native::widget::Tooltip<'a, Message, crate::Renderer>; +} + +pub use iced_native::widget::progress_bar; +pub use iced_native::widget::rule; +pub use iced_native::widget::slider; +pub use iced_native::widget::Space; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use container::Container; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tooltip::Tooltip; + +#[cfg(feature = "canvas")] +pub use iced_graphics::widget::canvas; + +#[cfg(feature = "image")] pub mod image { //! Display images in your user interface. - pub use crate::runtime::image::Handle; - pub use crate::runtime::widget::image::viewer; - pub use crate::runtime::widget::image::{Image, Viewer}; + pub use iced_native::image::Handle; + + /// A frame that displays an image. + pub type Image = iced_native::widget::Image<Handle>; + + pub use iced_native::widget::image::viewer; + pub use viewer::Viewer; } -#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +#[cfg(feature = "qr_code")] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "svg")] pub mod svg { - //! Display vector graphics in your user interface. - pub use crate::runtime::svg::Handle; - pub use crate::runtime::widget::svg::Svg; + //! Display vector graphics in your application. + pub use iced_native::svg::Handle; + pub use iced_native::widget::Svg; } -#[doc(no_inline)] -pub use { - button::Button, checkbox::Checkbox, container::Container, image::Image, - pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, - radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, svg::Svg, - text_input::TextInput, toggler::Toggler, tooltip::Tooltip, -}; - -#[cfg(any(feature = "canvas", feature = "glow_canvas"))] -#[doc(no_inline)] +#[cfg(feature = "canvas")] pub use canvas::Canvas; -#[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] -#[doc(no_inline)] +#[cfg(feature = "image")] +pub use image::Image; + +#[cfg(feature = "qr_code")] pub use qr_code::QRCode; + +#[cfg(feature = "svg")] +pub use svg::Svg; diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index fb03854b..5d4f5edd 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -32,7 +32,6 @@ pub mod settings; pub mod triangle; -pub mod widget; pub mod window; mod backend; @@ -45,9 +44,6 @@ pub use wgpu; pub use backend::Backend; pub use settings::Settings; -#[doc(no_inline)] -pub use widget::*; - pub(crate) use iced_graphics::Transformation; #[cfg(any(feature = "image_rs", feature = "svg"))] diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs deleted file mode 100644 index 99ae0ac2..00000000 --- a/wgpu/src/widget.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_wgpu::{button, Button}; -//! ``` -use crate::Renderer; - -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 rule; -pub mod scrollable; -pub mod slider; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -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; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - -pub use iced_native::widget::Space; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message> = - iced_native::widget::Column<'a, Message, Renderer>; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message> = iced_native::widget::Row<'a, Message, Renderer>; - -/// A paragraph of text. -pub type Text = iced_native::widget::Text<Renderer>; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs deleted file mode 100644 index f11ff25e..00000000 --- a/wgpu/src/widget/button.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::button::{Style, StyleSheet}; -pub use iced_native::widget::button::State; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message> = - iced_native::widget::Button<'a, Message, Renderer>; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs deleted file mode 100644 index 399dd19c..00000000 --- a/wgpu/src/widget/canvas.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub use iced_graphics::canvas::*; diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs deleted file mode 100644 index 76d572d9..00000000 --- a/wgpu/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_graphics::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message> = - iced_native::widget::Checkbox<'a, Message, Renderer>; diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs deleted file mode 100644 index c16db50d..00000000 --- a/wgpu/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_graphics::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message> = - iced_native::widget::Container<'a, Message, Renderer>; diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs deleted file mode 100644 index 38bdb672..00000000 --- a/wgpu/src/widget/pane_grid.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_graphics::pane_grid::{ - Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent, - Split, State, StyleSheet, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message> = - iced_native::widget::PaneGrid<'a, Message, Renderer>; - -/// The content of a [`Pane`]. -pub type Content<'a, Message> = - iced_native::widget::pane_grid::Content<'a, Message, Renderer>; - -/// The title bar of a [`Pane`]. -pub type TitleBar<'a, Message> = - iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/wgpu/src/widget/pick_list.rs b/wgpu/src/widget/pick_list.rs deleted file mode 100644 index 4d93be68..00000000 --- a/wgpu/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -pub use iced_native::widget::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::widget::PickList<'a, T, Message, crate::Renderer>; diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs deleted file mode 100644 index 88391ccb..00000000 --- a/wgpu/src/widget/progress_bar.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. -pub use iced_graphics::progress_bar::*; diff --git a/wgpu/src/widget/qr_code.rs b/wgpu/src/widget/qr_code.rs deleted file mode 100644 index 7b1c2408..00000000 --- a/wgpu/src/widget/qr_code.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Encode and display information in a QR code. -pub use iced_graphics::qr_code::*; diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs deleted file mode 100644 index 9ef1d7a5..00000000 --- a/wgpu/src/widget/radio.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_graphics::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message> = iced_native::widget::Radio<'a, Message, Renderer>; diff --git a/wgpu/src/widget/rule.rs b/wgpu/src/widget/rule.rs deleted file mode 100644 index 40281773..00000000 --- a/wgpu/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_graphics::rule::*; diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs deleted file mode 100644 index d5635ec5..00000000 --- a/wgpu/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; -pub use iced_native::widget::scrollable::State; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message> = - iced_native::widget::Scrollable<'a, Message, Renderer>; diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs deleted file mode 100644 index 2fb3d5d9..00000000 --- a/wgpu/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; -pub use iced_native::widget::slider::{Slider, State}; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs deleted file mode 100644 index 5560e3e0..00000000 --- a/wgpu/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::text_input::{Style, StyleSheet}; -pub use iced_native::widget::text_input::State; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message> = - iced_native::widget::TextInput<'a, Message, Renderer>; diff --git a/wgpu/src/widget/toggler.rs b/wgpu/src/widget/toggler.rs deleted file mode 100644 index 7ef5e22e..00000000 --- a/wgpu/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_graphics::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled -/// -/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message> = - iced_native::widget::Toggler<'a, Message, Renderer>; diff --git a/wgpu/src/widget/tooltip.rs b/wgpu/src/widget/tooltip.rs deleted file mode 100644 index c6af3903..00000000 --- a/wgpu/src/widget/tooltip.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Display a widget over another. -/// A widget allowing the selection of a single value from a list of options. -pub type Tooltip<'a, Message> = - iced_native::widget::Tooltip<'a, Message, crate::Renderer>; - -pub use iced_native::widget::tooltip::Position; |