From 8f0839e786f8d521f7319dd0e188d43284f526b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Feb 2022 19:42:15 +0700 Subject: [PATCH 01/55] Draft `iced_virtual` subcrate The idea here is to expose a set of "virtual widgets" that can be used with a `Virtual` widget and its `virtual::State`. A virtual widget is a widget that does not contain any state, but instead is a "virtual" representation of the "real" widget. The real widgets are stored in the `virtual::State`. Every time a new virtual widget tree is created during `view`, it is compared to the previous one and "real" widgets are added / removed to the `virtual::State`. Effectively, this removes the need to keep track of local widget state in the application state and allows `view` to take an immutable reference to `self`. To summarize, using this crate should allow users to remove `State` structs in their application state. Eventually, the strategy used here may be adopted generally and, as a result, all of the widgets in `iced_native` would be replaced! --- Cargo.toml | 1 + virtual/Cargo.toml | 8 +++++ virtual/src/element.rs | 37 +++++++++++++++++++++ virtual/src/lib.rs | 61 ++++++++++++++++++++++++++++++++++ virtual/src/widget.rs | 52 +++++++++++++++++++++++++++++ virtual/src/widget/button.rs | 64 ++++++++++++++++++++++++++++++++++++ 6 files changed, 223 insertions(+) create mode 100644 virtual/Cargo.toml create mode 100644 virtual/src/element.rs create mode 100644 virtual/src/lib.rs create mode 100644 virtual/src/widget.rs create mode 100644 virtual/src/widget/button.rs diff --git a/Cargo.toml b/Cargo.toml index 7c222fbb2d..7a4cecc88c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "lazy", "native", "style", + "virtual", "wgpu", "winit", "examples/bezier_tool", diff --git a/virtual/Cargo.toml b/virtual/Cargo.toml new file mode 100644 index 0000000000..74960b8011 --- /dev/null +++ b/virtual/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "iced_virtual" +version = "0.1.0" +edition = "2021" + +[dependencies] +iced_native = { version = "0.4", path = "../native" } +iced_style = { version = "0.3", path = "../style" } diff --git a/virtual/src/element.rs b/virtual/src/element.rs new file mode 100644 index 0000000000..ecdb067784 --- /dev/null +++ b/virtual/src/element.rs @@ -0,0 +1,37 @@ +use crate::Widget; + +pub struct Element { + widget: Box>, +} + +impl Element { + pub fn new(widget: impl Descriptor + 'static) -> Self { + Self { + widget: Box::new(widget), + } + } +} + +pub trait Descriptor { + fn tag(&self) -> std::any::TypeId; + + fn build(&self) -> Box>; + + fn children(&self) -> &[Element]; + + fn clone(&self) -> Box>; +} + +impl Clone for Box> { + fn clone(&self) -> Self { + self.as_ref().clone() + } +} + +impl Clone for Element { + fn clone(&self) -> Self { + Element { + widget: self.widget.clone(), + } + } +} diff --git a/virtual/src/lib.rs b/virtual/src/lib.rs new file mode 100644 index 0000000000..8095387244 --- /dev/null +++ b/virtual/src/lib.rs @@ -0,0 +1,61 @@ +mod element; +pub mod widget; + +pub use element::Element; +pub use widget::Widget; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::{Hasher, Length, Point, Rectangle}; + +pub struct Virtual<'a, Message, Renderer> { + state: &'a mut State, +} + +pub struct State { + widget_tree: widget::Tree, + last_element: Element, +} + +impl<'a, Message, Renderer> iced_native::Widget + for Virtual<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + self.state.widget_tree.width() + } + + fn height(&self) -> Length { + self.state.widget_tree.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.state.widget_tree.layout(renderer, limits) + } + + fn hash_layout(&self, state: &mut Hasher) { + self.state.widget_tree.hash_layout(state) + } + + fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.state.widget_tree.draw( + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} diff --git a/virtual/src/widget.rs b/virtual/src/widget.rs new file mode 100644 index 0000000000..f8697bc045 --- /dev/null +++ b/virtual/src/widget.rs @@ -0,0 +1,52 @@ +mod button; + +pub use button::Button; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::{Hasher, Length, Point, Rectangle}; + +pub trait Widget {} + +pub(crate) enum Tree { + Node { + widget: Box>, + children: Vec>, + }, + Leaf { + widget: Box>, + }, +} + +impl Tree { + pub fn width(&self) -> Length { + unimplemented! {} + } + + pub fn height(&self) -> Length { + unimplemented! {} + } + + pub fn hash_layout(&self, state: &mut Hasher) { + unimplemented! {} + } + + pub fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + unimplemented! {} + } + + pub fn draw( + &self, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + unimplemented! {} + } +} diff --git a/virtual/src/widget/button.rs b/virtual/src/widget/button.rs new file mode 100644 index 0000000000..115be80b5f --- /dev/null +++ b/virtual/src/widget/button.rs @@ -0,0 +1,64 @@ +use crate::element::{self, Element}; +use crate::Widget; + +pub struct Button { + content: Element, + on_press: Option, +} + +impl Button { + pub fn new( + content: impl element::Descriptor + 'static, + ) -> Self { + Button { + content: Element::new(content), + on_press: None, + } + } + + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } +} + +impl element::Descriptor + for Button +where + Message: 'static + Clone, + Renderer: 'static, +{ + fn tag(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + + fn build(&self) -> Box> { + Box::new(State { is_pressed: false }) + } + + fn children(&self) -> &[Element] { + std::slice::from_ref(&self.content) + } + + fn clone(&self) -> Box> { + Box::new(Clone::clone(self)) + } +} + +impl Clone for Button +where + Message: Clone, +{ + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + on_press: self.on_press.clone(), + } + } +} + +pub struct State { + is_pressed: bool, +} + +impl Widget for State {} From 5225e0e304bf5b407977e549c48ce9dea26b8c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 10 Feb 2022 21:54:13 +0700 Subject: [PATCH 02/55] Draft virtual `Button`, `Column`, and `Text` ... as well as a very naive diffing strategy! --- Cargo.toml | 1 + examples/virtual_counter/Cargo.toml | 10 ++ examples/virtual_counter/README.md | 18 ++ examples/virtual_counter/index.html | 12 ++ examples/virtual_counter/src/main.rs | 61 +++++++ src/lib.rs | 1 + virtual/src/element.rs | 37 ---- virtual/src/flex.rs | 232 ++++++++++++++++++++++++ virtual/src/lib.rs | 107 +++++++++-- virtual/src/widget.rs | 81 +++++---- virtual/src/widget/button.rs | 254 ++++++++++++++++++++++++--- virtual/src/widget/column.rs | 209 ++++++++++++++++++++++ virtual/src/widget/element.rs | 21 +++ virtual/src/widget/text.rs | 176 +++++++++++++++++++ virtual/src/widget/tree.rs | 58 ++++++ 15 files changed, 1177 insertions(+), 101 deletions(-) create mode 100644 examples/virtual_counter/Cargo.toml create mode 100644 examples/virtual_counter/README.md create mode 100644 examples/virtual_counter/index.html create mode 100644 examples/virtual_counter/src/main.rs delete mode 100644 virtual/src/element.rs create mode 100644 virtual/src/flex.rs create mode 100644 virtual/src/widget/column.rs create mode 100644 virtual/src/widget/element.rs create mode 100644 virtual/src/widget/text.rs create mode 100644 virtual/src/widget/tree.rs diff --git a/Cargo.toml b/Cargo.toml index 7a4cecc88c..1d4eb8831c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ members = [ "examples/tooltip", "examples/tour", "examples/url_handler", + "examples/virtual_counter", "examples/websocket", ] diff --git a/examples/virtual_counter/Cargo.toml b/examples/virtual_counter/Cargo.toml new file mode 100644 index 0000000000..537d4174a1 --- /dev/null +++ b/examples/virtual_counter/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "virtual_counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_virtual = { path = "../../virtual" } diff --git a/examples/virtual_counter/README.md b/examples/virtual_counter/README.md new file mode 100644 index 0000000000..4d9fc5b979 --- /dev/null +++ b/examples/virtual_counter/README.md @@ -0,0 +1,18 @@ +## Counter + +The classic counter example explained in the [`README`](../../README.md). + +The __[`main`]__ file contains all the code of the example. + + + +You can run it with `cargo run`: +``` +cargo run --package counter +``` + +[`main`]: src/main.rs diff --git a/examples/virtual_counter/index.html b/examples/virtual_counter/index.html new file mode 100644 index 0000000000..d2e368e4eb --- /dev/null +++ b/examples/virtual_counter/index.html @@ -0,0 +1,12 @@ + + + + + + Counter - Iced + + + + + + diff --git a/examples/virtual_counter/src/main.rs b/examples/virtual_counter/src/main.rs new file mode 100644 index 0000000000..46e8320e3e --- /dev/null +++ b/examples/virtual_counter/src/main.rs @@ -0,0 +1,61 @@ +use iced::{Alignment, Element, Sandbox, Settings}; +use iced_virtual::{Button, Column, Text, Virtual}; + +pub fn main() -> iced::Result { + Counter::run(Settings::default()) +} + +struct Counter { + value: i32, + state: iced_virtual::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + IncrementPressed, + DecrementPressed, +} + +impl Sandbox for Counter { + type Message = Message; + + fn new() -> Self { + Self { + value: 0, + state: iced_virtual::State::new(), + } + } + + 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(&mut self) -> Element { + let content = Column::new() + .padding(20) + .align_items(Alignment::Center) + .push( + Button::new(Text::new("Increment")) + .on_press(Message::IncrementPressed), + ) + .push(Text::new(self.value.to_string()).size(50)) + .push( + Button::new(Text::new("Decrement")) + .on_press(Message::DecrementPressed), + ); + + Virtual::new(&mut self.state, iced_virtual::Element::new(content)) + .into() + } +} diff --git a/src/lib.rs b/src/lib.rs index c8047d7f3f..a110cd18a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -214,6 +214,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/virtual/src/element.rs b/virtual/src/element.rs deleted file mode 100644 index ecdb067784..0000000000 --- a/virtual/src/element.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::Widget; - -pub struct Element { - widget: Box>, -} - -impl Element { - pub fn new(widget: impl Descriptor + 'static) -> Self { - Self { - widget: Box::new(widget), - } - } -} - -pub trait Descriptor { - fn tag(&self) -> std::any::TypeId; - - fn build(&self) -> Box>; - - fn children(&self) -> &[Element]; - - fn clone(&self) -> Box>; -} - -impl Clone for Box> { - fn clone(&self) -> Self { - self.as_ref().clone() - } -} - -impl Clone for Element { - fn clone(&self) -> Self { - Element { - widget: self.widget.clone(), - } - } -} diff --git a/virtual/src/flex.rs b/virtual/src/flex.rs new file mode 100644 index 0000000000..8d473f084c --- /dev/null +++ b/virtual/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( + axis: Axis, + renderer: &Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[Element], +) -> 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 = 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/virtual/src/lib.rs b/virtual/src/lib.rs index 8095387244..bab0da15a6 100644 --- a/virtual/src/lib.rs +++ b/virtual/src/lib.rs @@ -1,33 +1,75 @@ -mod element; pub mod widget; -pub use element::Element; -pub use widget::Widget; +pub(crate) mod flex; +pub use widget::*; + +use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; +use iced_native::mouse; use iced_native::renderer; -use iced_native::{Hasher, Length, Point, Rectangle}; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; pub struct Virtual<'a, Message, Renderer> { state: &'a mut State, } +impl<'a, Message, Renderer> Virtual<'a, Message, Renderer> +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + pub fn new( + state: &'a mut State, + content: Element, + ) -> Self { + let _ = state.diff(content); + + Self { state } + } +} + pub struct State { - widget_tree: widget::Tree, + state_tree: widget::Tree, last_element: Element, } +impl State +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + pub fn new() -> Self { + let last_element = Element::new(widget::Column::new()); + + Self { + state_tree: widget::Tree::new(&last_element), + last_element, + } + } + + fn diff(&mut self, new_element: Element) { + self.state_tree.diff(&self.last_element, &new_element); + + self.last_element = new_element; + } +} + impl<'a, Message, Renderer> iced_native::Widget for Virtual<'a, Message, Renderer> where Renderer: iced_native::Renderer, { fn width(&self) -> Length { - self.state.widget_tree.width() + self.state.last_element.as_widget().width() } fn height(&self) -> Length { - self.state.widget_tree.height() + self.state.last_element.as_widget().height() + } + + fn hash_layout(&self, state: &mut Hasher) { + self.state.last_element.as_widget().hash_layout(state) } fn layout( @@ -35,11 +77,27 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.state.widget_tree.layout(renderer, limits) + self.state.last_element.as_widget().layout(renderer, limits) } - fn hash_layout(&self, state: &mut Hasher) { - self.state.widget_tree.hash_layout(state) + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.state.last_element.as_widget_mut().on_event( + &mut self.state.state_tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) } fn draw( @@ -50,7 +108,8 @@ where cursor_position: Point, viewport: &Rectangle, ) { - self.state.widget_tree.draw( + self.state.last_element.as_widget().draw( + &self.state.state_tree, renderer, style, layout, @@ -58,4 +117,30 @@ where viewport, ) } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.state.last_element.as_widget().mouse_interaction( + &self.state.state_tree, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into> + for Virtual<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn into(self) -> iced_native::Element<'a, Message, Renderer> { + iced_native::Element::new(self) + } } diff --git a/virtual/src/widget.rs b/virtual/src/widget.rs index f8697bc045..bf63b999d3 100644 --- a/virtual/src/widget.rs +++ b/virtual/src/widget.rs @@ -1,52 +1,73 @@ mod button; +mod column; +mod element; +mod text; +mod tree; pub use button::Button; +pub use column::Column; +pub use element::Element; +pub use text::Text; +pub use tree::Tree; +use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; +use iced_native::mouse; use iced_native::renderer; -use iced_native::{Hasher, Length, Point, Rectangle}; - -pub trait Widget {} - -pub(crate) enum Tree { - Node { - widget: Box>, - children: Vec>, - }, - Leaf { - widget: Box>, - }, -} +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -impl Tree { - pub fn width(&self) -> Length { - unimplemented! {} - } +use std::any::{self, Any}; - pub fn height(&self) -> Length { - unimplemented! {} - } +pub trait Widget { + fn tag(&self) -> any::TypeId; - pub fn hash_layout(&self, state: &mut Hasher) { - unimplemented! {} - } + fn state(&self) -> Box; + + fn children(&self) -> &[Element]; + + fn width(&self) -> Length; + + fn height(&self) -> Length; - pub fn layout( + fn hash_layout(&self, state: &mut Hasher); + + fn layout( &self, renderer: &Renderer, limits: &layout::Limits, - ) -> layout::Node { - unimplemented! {} - } + ) -> layout::Node; - pub fn draw( + fn draw( &self, + state: &Tree, renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, - ) { - unimplemented! {} + ); + + 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 } } diff --git a/virtual/src/widget/button.rs b/virtual/src/widget/button.rs index 115be80b5f..534dd13a61 100644 --- a/virtual/src/widget/button.rs +++ b/virtual/src/widget/button.rs @@ -1,18 +1,36 @@ -use crate::element::{self, Element}; -use crate::Widget; +use crate::widget::{Element, Tree, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::touch; +use iced_native::{ + Background, Clipboard, Color, Hasher, Layout, Length, Padding, Point, + Rectangle, Shell, Vector, +}; +use iced_style::button::StyleSheet; + +use std::any::Any; pub struct Button { content: Element, on_press: Option, + style_sheet: Box, + width: Length, + height: Length, + padding: Padding, } impl Button { - pub fn new( - content: impl element::Descriptor + 'static, - ) -> Self { + pub fn new(content: impl Widget + 'static) -> Self { Button { content: Element::new(content), on_press: None, + style_sheet: Default::default(), + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(5), } } @@ -22,17 +40,16 @@ impl Button { } } -impl element::Descriptor - for Button +impl Widget for Button where Message: 'static + Clone, - Renderer: 'static, + Renderer: 'static + iced_native::Renderer, { fn tag(&self) -> std::any::TypeId { - std::any::TypeId::of::() + std::any::TypeId::of::() } - fn build(&self) -> Box> { + fn state(&self) -> Box { Box::new(State { is_pressed: false }) } @@ -40,25 +57,216 @@ where std::slice::from_ref(&self.content) } - fn clone(&self) -> Box> { - Box::new(Clone::clone(self)) + fn width(&self) -> Length { + self.width } -} -impl Clone for Button -where - Message: Clone, -{ - fn clone(&self) -> Self { - Self { - content: self.content.clone(), - on_press: self.on_press.clone(), + fn height(&self) -> Length { + self.height + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.tag().hash(state); + self.width.hash(state); + self.content.as_widget().hash_layout(state); + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.width) + .height(self.height) + .pad(self.padding); + + let mut content = self.content.as_widget().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]) + } + + 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 state = if let Some(state) = tree.state.downcast_mut::() { + state + } else { + return event::Status::Ignored; + }; + + 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; + } + + 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) { + 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 state.is_pressed { + state.is_pressed = false; + + if bounds.contains(cursor_position) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let state = if let Some(state) = tree.state.downcast_ref::() { + state + } else { + return; + }; + + 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 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)), + ); + } + + 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 { + 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() } } } -pub struct State { +#[derive(Debug, Clone)] +struct State { is_pressed: bool, } -impl Widget for State {} +impl Into> + for Button +where + Message: Clone + 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element { + Element::new(self) + } +} diff --git a/virtual/src/widget/column.rs b/virtual/src/widget/column.rs new file mode 100644 index 0000000000..e7649bc145 --- /dev/null +++ b/virtual/src/widget/column.rs @@ -0,0 +1,209 @@ +use crate::flex; +use crate::widget::{Element, Tree, 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, Hasher, Length, Padding, Point, Rectangle, Shell, +}; + +use std::any::{self, Any}; + +pub struct Column { + spacing: u16, + padding: Padding, + width: Length, + height: Length, + align_items: Alignment, + children: Vec>, +} + +impl<'a, Message, Renderer> Column { + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + pub fn with_children(children: Vec>) -> Self { + Column { + 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>(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>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl Widget for Column +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + struct Marker; + any::TypeId::of::() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children(&self) -> &[Element] { + &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::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 hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.tag().hash(state); + self.width.hash(state); + self.height.hash(state); + self.align_items.hash(state); + self.spacing.hash(state); + self.padding.hash(state); + + for child in &self.children { + child.as_widget().hash_layout(state); + } + } +} diff --git a/virtual/src/widget/element.rs b/virtual/src/widget/element.rs new file mode 100644 index 0000000000..5e40d488fd --- /dev/null +++ b/virtual/src/widget/element.rs @@ -0,0 +1,21 @@ +use crate::Widget; + +pub struct Element { + widget: Box>, +} + +impl Element { + pub fn new(widget: impl Widget + 'static) -> Self { + Self { + widget: Box::new(widget), + } + } + + pub fn as_widget(&self) -> &dyn Widget { + self.widget.as_ref() + } + + pub fn as_widget_mut(&mut self) -> &mut dyn Widget { + self.widget.as_mut() + } +} diff --git a/virtual/src/widget/text.rs b/virtual/src/widget/text.rs new file mode 100644 index 0000000000..91da7e9973 --- /dev/null +++ b/virtual/src/widget/text.rs @@ -0,0 +1,176 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::alignment; +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Color, Hasher, Length, Point, Rectangle, Size}; + +use std::any::{self, Any}; + +pub struct Text +where + Renderer: text::Renderer, +{ + content: String, + size: Option, + color: Option, + font: Renderer::Font, + width: Length, + height: Length, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +} + +impl Text { + /// Create a new fragment of [`Text`] with the given contents. + pub fn new>(label: T) -> Self { + Text { + content: label.into(), + size: None, + color: None, + font: Default::default(), + width: Length::Shrink, + height: Length::Shrink, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + } + } + + /// Sets the size of the [`Text`]. + pub fn size(mut self, size: u16) -> Self { + self.size = Some(size); + self + } + + /// Sets the [`Color`] of the [`Text`]. + pub fn color>(mut self, color: C) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the [`Font`] of the [`Text`]. + /// + /// [`Font`]: Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = font.into(); + self + } + + /// Sets the width of the [`Text`] boundaries. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Text`] boundaries. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the [`HorizontalAlignment`] of the [`Text`]. + pub fn horizontal_alignment( + mut self, + alignment: alignment::Horizontal, + ) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the [`VerticalAlignment`] of the [`Text`]. + pub fn vertical_alignment( + mut self, + alignment: alignment::Vertical, + ) -> Self { + self.vertical_alignment = alignment; + self + } +} + +impl Widget for Text +where + Renderer: text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children(&self) -> &[Element] { + &[] + } + + 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); + + let size = self.size.unwrap_or(renderer.default_size()); + + let bounds = limits.max(); + + let (width, height) = + renderer.measure(&self.content, size, self.font.clone(), bounds); + + let size = limits.resolve(Size::new(width, height)); + + layout::Node::new(size) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + iced_native::widget::text::draw( + renderer, + style, + layout, + &self.content, + self.font.clone(), + self.size, + self.color, + self.horizontal_alignment, + self.vertical_alignment, + ); + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + struct Marker; + std::any::TypeId::of::().hash(state); + + self.content.hash(state); + self.size.hash(state); + self.width.hash(state); + self.height.hash(state); + } +} + +impl Into> for Text +where + Renderer: text::Renderer + 'static, +{ + fn into(self) -> Element { + Element::new(self) + } +} diff --git a/virtual/src/widget/tree.rs b/virtual/src/widget/tree.rs new file mode 100644 index 0000000000..75f50a2fc8 --- /dev/null +++ b/virtual/src/widget/tree.rs @@ -0,0 +1,58 @@ +use crate::widget::Element; + +use std::any::Any; +use std::marker::PhantomData; + +pub struct Tree { + pub state: Box, + pub children: Vec>, + types_: PhantomData<(Message, Renderer)>, +} + +impl Tree { + pub fn new(element: &Element) -> Self { + Self { + state: element.as_widget().state(), + children: element + .as_widget() + .children() + .iter() + .map(Self::new) + .collect(), + types_: PhantomData, + } + } + + pub fn diff( + &mut self, + current: &Element, + new: &Element, + ) { + if current.as_widget().tag() == new.as_widget().tag() { + let current_children = current.as_widget().children(); + let new_children = new.as_widget().children(); + + if current_children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, (current, new)) in self + .children + .iter_mut() + .zip(current_children.iter().zip(new_children.iter())) + { + child_state.diff(current, new); + } + + if current_children.len() < new_children.len() { + self.children.extend( + new_children[current_children.len()..] + .iter() + .map(Self::new), + ); + } + } else { + *self = Self::new(new); + } + } +} From e03de019881f31d69b70a64c3e278ae5200d5080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 10 Feb 2022 23:16:21 +0700 Subject: [PATCH 03/55] Implement `Into` for `&'static str` in `iced_virtual` --- examples/virtual_counter/src/main.rs | 13 +++---------- virtual/src/lib.rs | 4 ++-- virtual/src/widget/button.rs | 4 ++-- virtual/src/widget/column.rs | 11 +++++++++++ virtual/src/widget/text.rs | 9 +++++++++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/virtual_counter/src/main.rs b/examples/virtual_counter/src/main.rs index 46e8320e3e..10d32112f7 100644 --- a/examples/virtual_counter/src/main.rs +++ b/examples/virtual_counter/src/main.rs @@ -45,17 +45,10 @@ impl Sandbox for Counter { let content = Column::new() .padding(20) .align_items(Alignment::Center) - .push( - Button::new(Text::new("Increment")) - .on_press(Message::IncrementPressed), - ) + .push(Button::new("Increment").on_press(Message::IncrementPressed)) .push(Text::new(self.value.to_string()).size(50)) - .push( - Button::new(Text::new("Decrement")) - .on_press(Message::DecrementPressed), - ); + .push(Button::new("Decrement").on_press(Message::DecrementPressed)); - Virtual::new(&mut self.state, iced_virtual::Element::new(content)) - .into() + Virtual::new(&mut self.state, content).into() } } diff --git a/virtual/src/lib.rs b/virtual/src/lib.rs index bab0da15a6..8ba4c328b0 100644 --- a/virtual/src/lib.rs +++ b/virtual/src/lib.rs @@ -21,9 +21,9 @@ where { pub fn new( state: &'a mut State, - content: Element, + content: impl Into>, ) -> Self { - let _ = state.diff(content); + let _ = state.diff(content.into()); Self { state } } diff --git a/virtual/src/widget/button.rs b/virtual/src/widget/button.rs index 534dd13a61..ece90811d1 100644 --- a/virtual/src/widget/button.rs +++ b/virtual/src/widget/button.rs @@ -23,9 +23,9 @@ pub struct Button { } impl Button { - pub fn new(content: impl Widget + 'static) -> Self { + pub fn new(content: impl Into>) -> Self { Button { - content: Element::new(content), + content: content.into(), on_press: None, style_sheet: Default::default(), width: Length::Shrink, diff --git a/virtual/src/widget/column.rs b/virtual/src/widget/column.rs index e7649bc145..2f70282aaa 100644 --- a/virtual/src/widget/column.rs +++ b/virtual/src/widget/column.rs @@ -207,3 +207,14 @@ where } } } + +impl Into> + for Column +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element { + Element::new(self) + } +} diff --git a/virtual/src/widget/text.rs b/virtual/src/widget/text.rs index 91da7e9973..e3a7d299af 100644 --- a/virtual/src/widget/text.rs +++ b/virtual/src/widget/text.rs @@ -174,3 +174,12 @@ where Element::new(self) } } + +impl Into> for &'static str +where + Renderer: text::Renderer + 'static, +{ + fn into(self) -> Element { + Text::new(self).into() + } +} From 897188317b5875cc00a0f1c797790df8ac13687f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 17:50:12 +0700 Subject: [PATCH 04/55] Rename `iced_virtual` to `iced_pure` `virtual` is a reserved keyword in Rust :grimacing: --- Cargo.toml | 4 ++-- examples/{virtual_counter => pure/counter}/Cargo.toml | 6 +++--- examples/{virtual_counter => pure/counter}/README.md | 0 examples/{virtual_counter => pure/counter}/index.html | 0 examples/{virtual_counter => pure/counter}/src/main.rs | 10 +++++----- {virtual => pure}/Cargo.toml | 2 +- pure/src/application.rs | 1 + {virtual => pure}/src/flex.rs | 0 {virtual => pure}/src/lib.rs | 8 ++++---- {virtual => pure}/src/widget.rs | 0 {virtual => pure}/src/widget/button.rs | 0 {virtual => pure}/src/widget/column.rs | 0 {virtual => pure}/src/widget/element.rs | 0 {virtual => pure}/src/widget/text.rs | 0 {virtual => pure}/src/widget/tree.rs | 0 15 files changed, 16 insertions(+), 15 deletions(-) rename examples/{virtual_counter => pure/counter}/Cargo.toml (59%) rename examples/{virtual_counter => pure/counter}/README.md (100%) rename examples/{virtual_counter => pure/counter}/index.html (100%) rename examples/{virtual_counter => pure/counter}/src/main.rs (80%) rename {virtual => pure}/Cargo.toml (88%) create mode 100644 pure/src/application.rs rename {virtual => pure}/src/flex.rs (100%) rename {virtual => pure}/src/lib.rs (94%) rename {virtual => pure}/src/widget.rs (100%) rename {virtual => pure}/src/widget/button.rs (100%) rename {virtual => pure}/src/widget/column.rs (100%) rename {virtual => pure}/src/widget/element.rs (100%) rename {virtual => pure}/src/widget/text.rs (100%) rename {virtual => pure}/src/widget/tree.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 1d4eb8831c..1918d8b43e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,8 +57,8 @@ members = [ "glutin", "lazy", "native", + "pure", "style", - "virtual", "wgpu", "winit", "examples/bezier_tool", @@ -88,7 +88,7 @@ members = [ "examples/tooltip", "examples/tour", "examples/url_handler", - "examples/virtual_counter", + "examples/pure/counter", "examples/websocket", ] diff --git a/examples/virtual_counter/Cargo.toml b/examples/pure/counter/Cargo.toml similarity index 59% rename from examples/virtual_counter/Cargo.toml rename to examples/pure/counter/Cargo.toml index 537d4174a1..1363bfd58f 100644 --- a/examples/virtual_counter/Cargo.toml +++ b/examples/pure/counter/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "virtual_counter" +name = "pure_counter" version = "0.1.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_virtual = { path = "../../virtual" } +iced = { path = "../../.." } +iced_pure = { path = "../../../pure" } diff --git a/examples/virtual_counter/README.md b/examples/pure/counter/README.md similarity index 100% rename from examples/virtual_counter/README.md rename to examples/pure/counter/README.md diff --git a/examples/virtual_counter/index.html b/examples/pure/counter/index.html similarity index 100% rename from examples/virtual_counter/index.html rename to examples/pure/counter/index.html diff --git a/examples/virtual_counter/src/main.rs b/examples/pure/counter/src/main.rs similarity index 80% rename from examples/virtual_counter/src/main.rs rename to examples/pure/counter/src/main.rs index 10d32112f7..e2746d405a 100644 --- a/examples/virtual_counter/src/main.rs +++ b/examples/pure/counter/src/main.rs @@ -1,5 +1,5 @@ use iced::{Alignment, Element, Sandbox, Settings}; -use iced_virtual::{Button, Column, Text, Virtual}; +use iced_pure::{Button, Column, Pure, State, Text}; pub fn main() -> iced::Result { Counter::run(Settings::default()) @@ -7,7 +7,7 @@ pub fn main() -> iced::Result { struct Counter { value: i32, - state: iced_virtual::State, + state: State, } #[derive(Debug, Clone, Copy)] @@ -22,7 +22,7 @@ impl Sandbox for Counter { fn new() -> Self { Self { value: 0, - state: iced_virtual::State::new(), + state: State::new(), } } @@ -41,7 +41,7 @@ impl Sandbox for Counter { } } - fn view(&mut self) -> Element { + fn view(&mut self) -> Element<'_, Message> { let content = Column::new() .padding(20) .align_items(Alignment::Center) @@ -49,6 +49,6 @@ impl Sandbox for Counter { .push(Text::new(self.value.to_string()).size(50)) .push(Button::new("Decrement").on_press(Message::DecrementPressed)); - Virtual::new(&mut self.state, content).into() + Pure::new(&mut self.state, content).into() } } diff --git a/virtual/Cargo.toml b/pure/Cargo.toml similarity index 88% rename from virtual/Cargo.toml rename to pure/Cargo.toml index 74960b8011..bdfe43ae79 100644 --- a/virtual/Cargo.toml +++ b/pure/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "iced_virtual" +name = "iced_pure" version = "0.1.0" edition = "2021" diff --git a/pure/src/application.rs b/pure/src/application.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/pure/src/application.rs @@ -0,0 +1 @@ + diff --git a/virtual/src/flex.rs b/pure/src/flex.rs similarity index 100% rename from virtual/src/flex.rs rename to pure/src/flex.rs diff --git a/virtual/src/lib.rs b/pure/src/lib.rs similarity index 94% rename from virtual/src/lib.rs rename to pure/src/lib.rs index 8ba4c328b0..4381bfc873 100644 --- a/virtual/src/lib.rs +++ b/pure/src/lib.rs @@ -10,11 +10,11 @@ use iced_native::mouse; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -pub struct Virtual<'a, Message, Renderer> { +pub struct Pure<'a, Message, Renderer> { state: &'a mut State, } -impl<'a, Message, Renderer> Virtual<'a, Message, Renderer> +impl<'a, Message, Renderer> Pure<'a, Message, Renderer> where Message: 'static, Renderer: iced_native::Renderer + 'static, @@ -56,7 +56,7 @@ where } impl<'a, Message, Renderer> iced_native::Widget - for Virtual<'a, Message, Renderer> + for Pure<'a, Message, Renderer> where Renderer: iced_native::Renderer, { @@ -136,7 +136,7 @@ where } impl<'a, Message, Renderer> Into> - for Virtual<'a, Message, Renderer> + for Pure<'a, Message, Renderer> where Renderer: iced_native::Renderer, { diff --git a/virtual/src/widget.rs b/pure/src/widget.rs similarity index 100% rename from virtual/src/widget.rs rename to pure/src/widget.rs diff --git a/virtual/src/widget/button.rs b/pure/src/widget/button.rs similarity index 100% rename from virtual/src/widget/button.rs rename to pure/src/widget/button.rs diff --git a/virtual/src/widget/column.rs b/pure/src/widget/column.rs similarity index 100% rename from virtual/src/widget/column.rs rename to pure/src/widget/column.rs diff --git a/virtual/src/widget/element.rs b/pure/src/widget/element.rs similarity index 100% rename from virtual/src/widget/element.rs rename to pure/src/widget/element.rs diff --git a/virtual/src/widget/text.rs b/pure/src/widget/text.rs similarity index 100% rename from virtual/src/widget/text.rs rename to pure/src/widget/text.rs diff --git a/virtual/src/widget/tree.rs b/pure/src/widget/tree.rs similarity index 100% rename from virtual/src/widget/tree.rs rename to pure/src/widget/tree.rs From 66d69b5c9a183091e05e82bbe21b3203f75c1b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 17:51:33 +0700 Subject: [PATCH 05/55] Expose `iced_pure` through a `pure` feature in `iced` Besides exposing the `iced_pure` crate, enabling the `pure` feature also provides pure versions of both the `Application` and `Sandbox` traits! :tada: --- Cargo.toml | 3 + examples/pure/counter/Cargo.toml | 3 +- examples/pure/counter/src/main.rs | 19 ++-- src/lib.rs | 3 + src/pure.rs | 29 +++++ src/pure/application.rs | 182 ++++++++++++++++++++++++++++++ src/pure/sandbox.rs | 119 +++++++++++++++++++ 7 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 src/pure.rs create mode 100644 src/pure/application.rs create mode 100644 src/pure/sandbox.rs diff --git a/Cargo.toml b/Cargo.toml index 1918d8b43e..1527c3732a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,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"] [badges] maintenance = { status = "actively-developed" } @@ -98,6 +100,7 @@ iced_futures = { version = "0.3", path = "futures" } 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/pure/counter/Cargo.toml b/examples/pure/counter/Cargo.toml index 1363bfd58f..2fcd22d4fd 100644 --- a/examples/pure/counter/Cargo.toml +++ b/examples/pure/counter/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../../.." } -iced_pure = { path = "../../../pure" } +iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs index e2746d405a..00cb3fc742 100644 --- a/examples/pure/counter/src/main.rs +++ b/examples/pure/counter/src/main.rs @@ -1,5 +1,5 @@ -use iced::{Alignment, Element, Sandbox, Settings}; -use iced_pure::{Button, Column, Pure, State, Text}; +use iced::pure::{Button, Column, Element, Sandbox, Text}; +use iced::{Alignment, Settings}; pub fn main() -> iced::Result { Counter::run(Settings::default()) @@ -7,7 +7,6 @@ pub fn main() -> iced::Result { struct Counter { value: i32, - state: State, } #[derive(Debug, Clone, Copy)] @@ -20,10 +19,7 @@ impl Sandbox for Counter { type Message = Message; fn new() -> Self { - Self { - value: 0, - state: State::new(), - } + Self { value: 0 } } fn title(&self) -> String { @@ -41,14 +37,13 @@ impl Sandbox for Counter { } } - fn view(&mut self) -> Element<'_, Message> { - let content = Column::new() + fn view(&self) -> Element { + Column::new() .padding(20) .align_items(Alignment::Center) .push(Button::new("Increment").on_press(Message::IncrementPressed)) .push(Text::new(self.value.to_string()).size(50)) - .push(Button::new("Decrement").on_press(Message::DecrementPressed)); - - Pure::new(&mut self.state, content).into() + .push(Button::new("Decrement").on_press(Message::DecrementPressed)) + .into() } } diff --git a/src/lib.rs b/src/lib.rs index a110cd18a2..ab4c503299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/pure.rs b/src/pure.rs new file mode 100644 index 0000000000..94d0d2a07f --- /dev/null +++ b/src/pure.rs @@ -0,0 +1,29 @@ +//! 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 use iced_pure::{Element as _, *}; + +/// A generic, pure [`Widget`]. +pub type Element = iced_pure::Element; + +mod application; +mod sandbox; + +pub use application::Application; +pub use sandbox::Sandbox; diff --git a/src/pure/application.rs b/src/pure/application.rs new file mode 100644 index 0000000000..973af9f75f --- /dev/null +++ b/src/pure/application.rs @@ -0,0 +1,182 @@ +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); + + /// 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; + + /// 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 { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> pure::Element; + + /// 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) -> crate::Result + where + Self: 'static, + { + as crate::Application>::run(settings) + } +} + +struct Instance { + application: A, + state: pure::State, +} + +impl crate::Application for Instance +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) { + 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 { + A::update(&mut self.application, message) + } + + 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 0000000000..def90b6be8 --- /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; + + /// 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, + { + ::run(settings) + } +} + +impl 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::new(), Command::none()) + } + + fn title(&self) -> String { + T::title(self) + } + + fn update(&mut self, message: T::Message) -> Command { + T::update(self, message); + + Command::none() + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + fn view(&self) -> pure::Element { + 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) + } +} From 43a7ad72ef070929278e6d03d98077ac267fe2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 18:42:15 +0700 Subject: [PATCH 06/55] Expose function helpers to build widgets in `pure::widget` `button("Hello")` is easier to write and read than `Button::new("Hello")`. --- examples/pure/counter/src/main.rs | 11 ++++++----- pure/src/widget.rs | 17 +++++++++++++++++ pure/src/widget/text.rs | 4 ++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs index 00cb3fc742..4cb79a0eb0 100644 --- a/examples/pure/counter/src/main.rs +++ b/examples/pure/counter/src/main.rs @@ -1,4 +1,5 @@ -use iced::pure::{Button, Column, Element, Sandbox, Text}; +use iced::pure::widget::{button, column, text}; +use iced::pure::{Element, Sandbox}; use iced::{Alignment, Settings}; pub fn main() -> iced::Result { @@ -38,12 +39,12 @@ impl Sandbox for Counter { } fn view(&self) -> Element { - Column::new() + column() .padding(20) .align_items(Alignment::Center) - .push(Button::new("Increment").on_press(Message::IncrementPressed)) - .push(Text::new(self.value.to_string()).size(50)) - .push(Button::new("Decrement").on_press(Message::DecrementPressed)) + .push(button("Increment").on_press(Message::IncrementPressed)) + .push(text(self.value).size(50)) + .push(button("Decrement").on_press(Message::DecrementPressed)) .into() } } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index bf63b999d3..7215e99e5d 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -71,3 +71,20 @@ pub trait Widget { event::Status::Ignored } } + +pub fn column() -> Column { + Column::new() +} + +pub fn button( + content: impl Into>, +) -> Button { + Button::new(content) +} + +pub fn text(text: impl ToString) -> Text +where + Renderer: iced_native::text::Renderer, +{ + Text::new(text) +} diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index e3a7d299af..73ff71e245 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -24,9 +24,9 @@ where impl Text { /// Create a new fragment of [`Text`] with the given contents. - pub fn new>(label: T) -> Self { + pub fn new(label: T) -> Self { Text { - content: label.into(), + content: label.to_string(), size: None, color: None, font: Default::default(), From 01c5004959c9b11f2580840f4553ad7d706f4564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 22:07:21 +0700 Subject: [PATCH 07/55] Allow pure widgets to borrow from `Application` data :tada: --- pure/src/application.rs | 1 - pure/src/lib.rs | 54 ++++++++++++++++++-------------------- pure/src/widget.rs | 14 +++++----- pure/src/widget/button.rs | 25 +++++++++--------- pure/src/widget/column.rs | 27 ++++++++++--------- pure/src/widget/element.rs | 8 +++--- pure/src/widget/text.rs | 12 +++++---- pure/src/widget/tree.rs | 49 ++++++++++++++++++---------------- src/pure.rs | 3 ++- src/pure/application.rs | 4 +-- src/pure/sandbox.rs | 4 +-- 11 files changed, 103 insertions(+), 98 deletions(-) delete mode 100644 pure/src/application.rs diff --git a/pure/src/application.rs b/pure/src/application.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/pure/src/application.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pure/src/lib.rs b/pure/src/lib.rs index 4381bfc873..a179a84b05 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -11,7 +11,8 @@ use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; pub struct Pure<'a, Message, Renderer> { - state: &'a mut State, + state: &'a mut State, + element: Element<'a, Message, Renderer>, } impl<'a, Message, Renderer> Pure<'a, Message, Renderer> @@ -20,38 +21,32 @@ where Renderer: iced_native::Renderer + 'static, { pub fn new( - state: &'a mut State, - content: impl Into>, + state: &'a mut State, + content: impl Into>, ) -> Self { - let _ = state.diff(content.into()); + let element = content.into(); + let _ = state.diff(&element); - Self { state } + Self { state, element } } } -pub struct State { - state_tree: widget::Tree, - last_element: Element, +pub struct State { + state_tree: widget::Tree, } -impl State -where - Message: 'static, - Renderer: iced_native::Renderer + 'static, -{ +impl State { pub fn new() -> Self { - let last_element = Element::new(widget::Column::new()); - Self { - state_tree: widget::Tree::new(&last_element), - last_element, + state_tree: widget::Tree::empty(), } } - fn diff(&mut self, new_element: Element) { - self.state_tree.diff(&self.last_element, &new_element); - - self.last_element = new_element; + fn diff( + &mut self, + new_element: &Element, + ) { + self.state_tree.diff(new_element); } } @@ -61,15 +56,15 @@ where Renderer: iced_native::Renderer, { fn width(&self) -> Length { - self.state.last_element.as_widget().width() + self.element.as_widget().width() } fn height(&self) -> Length { - self.state.last_element.as_widget().height() + self.element.as_widget().height() } fn hash_layout(&self, state: &mut Hasher) { - self.state.last_element.as_widget().hash_layout(state) + self.element.as_widget().hash_layout(state) } fn layout( @@ -77,7 +72,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.state.last_element.as_widget().layout(renderer, limits) + self.element.as_widget().layout(renderer, limits) } fn on_event( @@ -89,7 +84,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.state.last_element.as_widget_mut().on_event( + self.element.as_widget_mut().on_event( &mut self.state.state_tree, event, layout, @@ -108,7 +103,7 @@ where cursor_position: Point, viewport: &Rectangle, ) { - self.state.last_element.as_widget().draw( + self.element.as_widget().draw( &self.state.state_tree, renderer, style, @@ -125,7 +120,7 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.state.last_element.as_widget().mouse_interaction( + self.element.as_widget().mouse_interaction( &self.state.state_tree, layout, cursor_position, @@ -138,7 +133,8 @@ where impl<'a, Message, Renderer> Into> for Pure<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + 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/widget.rs b/pure/src/widget.rs index 7215e99e5d..9a4dffe3a1 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -39,7 +39,7 @@ pub trait Widget { fn draw( &self, - state: &Tree, + state: &Tree, renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, @@ -49,7 +49,7 @@ pub trait Widget { fn mouse_interaction( &self, - _state: &Tree, + _state: &Tree, _layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, @@ -60,7 +60,7 @@ pub trait Widget { fn on_event( &mut self, - _state: &mut Tree, + _state: &mut Tree, _event: Event, _layout: Layout<'_>, _cursor_position: Point, @@ -72,13 +72,13 @@ pub trait Widget { } } -pub fn column() -> Column { +pub fn column<'a, Message, Renderer>() -> Column<'a, Message, Renderer> { Column::new() } -pub fn button( - content: impl Into>, -) -> Button { +pub fn button<'a, Message, Renderer>( + content: impl Into>, +) -> Button<'a, Message, Renderer> { Button::new(content) } diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index ece90811d1..198a3af947 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -13,17 +13,17 @@ use iced_style::button::StyleSheet; use std::any::Any; -pub struct Button { - content: Element, +pub struct Button<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, on_press: Option, - style_sheet: Box, + style_sheet: Box, width: Length, height: Length, padding: Padding, } -impl Button { - pub fn new(content: impl Into>) -> Self { +impl<'a, Message, Renderer> Button<'a, Message, Renderer> { + pub fn new(content: impl Into>) -> Self { Button { content: content.into(), on_press: None, @@ -40,7 +40,8 @@ impl Button { } } -impl Widget for Button +impl<'a, Message, Renderer> Widget + for Button<'a, Message, Renderer> where Message: 'static + Clone, Renderer: 'static + iced_native::Renderer, @@ -96,7 +97,7 @@ where fn on_event( &mut self, - tree: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -162,7 +163,7 @@ where fn draw( &self, - tree: &Tree, + tree: &Tree, renderer: &mut Renderer, _style: &renderer::Style, layout: Layout<'_>, @@ -238,7 +239,7 @@ where fn mouse_interaction( &self, - _tree: &Tree, + _tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -260,13 +261,13 @@ struct State { is_pressed: bool, } -impl Into> - for Button +impl<'a, Message, Renderer> Into> + for Button<'a, Message, Renderer> where Message: Clone + 'static, Renderer: iced_native::Renderer + 'static, { - fn into(self) -> Element { + fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) } } diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 2f70282aaa..716fd714ae 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -11,21 +11,23 @@ use iced_native::{ use std::any::{self, Any}; -pub struct Column { +pub struct Column<'a, Message, Renderer> { spacing: u16, padding: Padding, width: Length, height: Length, align_items: Alignment, - children: Vec>, + children: Vec>, } -impl<'a, Message, Renderer> Column { +impl<'a, Message, Renderer> Column<'a, Message, Renderer> { pub fn new() -> Self { Self::with_children(Vec::new()) } - pub fn with_children(children: Vec>) -> Self { + pub fn with_children( + children: Vec>, + ) -> Self { Column { spacing: 0, padding: Padding::ZERO, @@ -63,14 +65,15 @@ impl<'a, Message, Renderer> Column { pub fn push( mut self, - child: impl Into>, + child: impl Into>, ) -> Self { self.children.push(child.into()); self } } -impl Widget for Column +impl<'a, Message, Renderer> Widget + for Column<'a, Message, Renderer> where Renderer: iced_native::Renderer, { @@ -115,7 +118,7 @@ where fn on_event( &mut self, - tree: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -143,7 +146,7 @@ where fn mouse_interaction( &self, - tree: &Tree, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -168,7 +171,7 @@ where fn draw( &self, - tree: &Tree, + tree: &Tree, renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, @@ -208,13 +211,13 @@ where } } -impl Into> - for Column +impl<'a, Message, Renderer> Into> + for Column<'a, Message, Renderer> where Message: 'static, Renderer: iced_native::Renderer + 'static, { - fn into(self) -> Element { + fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) } } diff --git a/pure/src/widget/element.rs b/pure/src/widget/element.rs index 5e40d488fd..aedf597383 100644 --- a/pure/src/widget/element.rs +++ b/pure/src/widget/element.rs @@ -1,11 +1,11 @@ use crate::Widget; -pub struct Element { - widget: Box>, +pub struct Element<'a, Message, Renderer> { + widget: Box + 'a>, } -impl Element { - pub fn new(widget: impl Widget + 'static) -> Self { +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { + pub fn new(widget: impl Widget + 'a) -> Self { Self { widget: Box::new(widget), } diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index 73ff71e245..5a5f360e8b 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -133,7 +133,7 @@ where fn draw( &self, - _tree: &Tree, + _tree: &Tree, renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, @@ -166,20 +166,22 @@ where } } -impl Into> for Text +impl<'a, Message, Renderer> Into> + for Text where Renderer: text::Renderer + 'static, { - fn into(self) -> Element { + fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) } } -impl Into> for &'static str +impl<'a, Message, Renderer> Into> + for &'static str where Renderer: text::Renderer + 'static, { - fn into(self) -> Element { + fn into(self) -> Element<'a, Message, Renderer> { Text::new(self).into() } } diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 75f50a2fc8..2353edc546 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -1,17 +1,27 @@ use crate::widget::Element; -use std::any::Any; -use std::marker::PhantomData; +use std::any::{self, Any}; -pub struct Tree { +pub struct Tree { + pub tag: any::TypeId, pub state: Box, - pub children: Vec>, - types_: PhantomData<(Message, Renderer)>, + pub children: Vec, } -impl Tree { - pub fn new(element: &Element) -> Self { +impl Tree { + pub fn empty() -> Self { Self { + tag: any::TypeId::of::<()>(), + state: Box::new(()), + children: Vec::new(), + } + } + + pub fn new( + element: &Element<'_, Message, Renderer>, + ) -> Self { + Self { + tag: element.as_widget().tag(), state: element.as_widget().state(), children: element .as_widget() @@ -19,36 +29,29 @@ impl Tree { .iter() .map(Self::new) .collect(), - types_: PhantomData, } } - pub fn diff( + pub fn diff( &mut self, - current: &Element, - new: &Element, + new: &Element<'_, Message, Renderer>, ) { - if current.as_widget().tag() == new.as_widget().tag() { - let current_children = current.as_widget().children(); + if self.tag == new.as_widget().tag() { let new_children = new.as_widget().children(); - if current_children.len() > new_children.len() { + if self.children.len() > new_children.len() { self.children.truncate(new_children.len()); } - for (child_state, (current, new)) in self - .children - .iter_mut() - .zip(current_children.iter().zip(new_children.iter())) + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) { - child_state.diff(current, new); + child_state.diff(new); } - if current_children.len() < new_children.len() { + if self.children.len() < new_children.len() { self.children.extend( - new_children[current_children.len()..] - .iter() - .map(Self::new), + new_children[self.children.len()..].iter().map(Self::new), ); } } else { diff --git a/src/pure.rs b/src/pure.rs index 94d0d2a07f..7fe2bc59d0 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -20,7 +20,8 @@ pub use iced_pure::{Element as _, *}; /// A generic, pure [`Widget`]. -pub type Element = iced_pure::Element; +pub type Element<'a, Message> = + iced_pure::Element<'a, Message, crate::Renderer>; mod application; mod sandbox; diff --git a/src/pure/application.rs b/src/pure/application.rs index 973af9f75f..395eba9a92 100644 --- a/src/pure/application.rs +++ b/src/pure/application.rs @@ -66,7 +66,7 @@ pub trait Application: Sized { /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> pure::Element; + fn view(&self) -> pure::Element<'_, Self::Message>; /// Returns the current [`Application`] mode. /// @@ -126,7 +126,7 @@ pub trait Application: Sized { struct Instance { application: A, - state: pure::State, + state: pure::State, } impl crate::Application for Instance diff --git a/src/pure/sandbox.rs b/src/pure/sandbox.rs index def90b6be8..fbd1d7a8ab 100644 --- a/src/pure/sandbox.rs +++ b/src/pure/sandbox.rs @@ -32,7 +32,7 @@ pub trait Sandbox { /// Returns the widgets to display in the [`Sandbox`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> pure::Element; + fn view(&self) -> pure::Element<'_, Self::Message>; /// Returns the background color of the [`Sandbox`]. /// @@ -101,7 +101,7 @@ where Subscription::none() } - fn view(&self) -> pure::Element { + fn view(&self) -> pure::Element<'_, T::Message> { T::view(self) } From ecb3df8e018930c407e469ce2b8f4208a9d15426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 23:17:07 +0700 Subject: [PATCH 08/55] Expose reusable `Button` logic ... and reuse it in `iced_pure`! --- examples/stopwatch/src/main.rs | 2 +- examples/tour/src/main.rs | 2 +- native/src/widget/button.rs | 295 +++++++++++++++++++-------------- pure/src/widget/button.rs | 162 +++++------------- pure/src/widget/tree.rs | 14 ++ 5 files changed, 228 insertions(+), 247 deletions(-) diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index dc8a4de74d..377d7a2db6 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 d4b41310f3..b01a1ce256 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -726,7 +726,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/native/src/widget/button.rs b/native/src/widget/button.rs index b4a3adc31c..049b6544e6 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -63,8 +63,6 @@ pub struct Button<'a, Message, Renderer> { on_press: Option, width: Length, height: Length, - min_width: u32, - min_height: u32, padding: Padding, style_sheet: Box, } @@ -86,8 +84,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(), } @@ -105,18 +101,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>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -153,6 +137,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, + 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, + 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 for Button<'a, Message, Renderer> where @@ -172,22 +303,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( @@ -210,42 +333,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( @@ -255,14 +350,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( @@ -276,51 +364,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, @@ -338,6 +389,8 @@ where std::any::TypeId::of::().hash(state); self.width.hash(state); + self.height.hash(state); + self.padding.hash(state); self.content.hash_layout(state); } diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 198a3af947..89acb7f52f 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -4,15 +4,16 @@ use iced_native::event::{self, Event}; use iced_native::layout; use iced_native::mouse; use iced_native::renderer; -use iced_native::touch; +use iced_native::widget::button; use iced_native::{ - Background, Clipboard, Color, Hasher, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, + Clipboard, Hasher, Layout, Length, Padding, Point, Rectangle, Shell, }; use iced_style::button::StyleSheet; use std::any::Any; +pub use button::State; + pub struct Button<'a, Message, Renderer> { content: Element<'a, Message, Renderer>, on_press: Option, @@ -51,7 +52,7 @@ where } fn state(&self) -> Box { - Box::new(State { is_pressed: false }) + Box::new(State::new()) } fn children(&self) -> &[Element] { @@ -71,6 +72,8 @@ where self.tag().hash(state); self.width.hash(state); + self.height.hash(state); + self.padding.hash(state); self.content.as_widget().hash_layout(state); } @@ -79,20 +82,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.as_widget().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]) + button::layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, &limits) + }, + ) } fn on_event( @@ -105,12 +104,6 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let state = if let Some(state) = tree.state.downcast_mut::() { - state - } else { - return event::Status::Ignored; - }; - if let event::Status::Captured = self.content.as_widget_mut().on_event( &mut tree.children[0], event.clone(), @@ -123,42 +116,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) { - 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 state.is_pressed { - state.is_pressed = false; - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored + button::update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || tree.state_mut::(), + ) } fn draw( @@ -170,60 +135,17 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let state = if let Some(state) = tree.state.downcast_ref::() { - state - } else { - return; - }; - 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 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 = button::draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + self.style_sheet.as_ref(), + || tree.state::(), + ); self.content.as_widget().draw( &tree.children[0], @@ -245,22 +167,14 @@ 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() - } + button::mouse_interaction( + layout, + cursor_position, + self.on_press.is_some(), + ) } } -#[derive(Debug, Clone)] -struct State { - is_pressed: bool, -} - impl<'a, Message, Renderer> Into> for Button<'a, Message, Renderer> where diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 2353edc546..1ab6d80b88 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -58,4 +58,18 @@ impl Tree { *self = Self::new(new); } } + + pub fn state(&self) -> &T + where + T: 'static, + { + self.state.downcast_ref().expect("Downcast widget state") + } + + pub fn state_mut(&mut self) -> &mut T + where + T: 'static, + { + self.state.downcast_mut().expect("Downcast widget state") + } } From dd3e74e74de3a416d9d2dcfee051d78ba03dc540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 23:33:03 +0700 Subject: [PATCH 09/55] Complete `Button` in `iced_pure` --- pure/src/widget/button.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 89acb7f52f..b9561b0941 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -35,8 +35,38 @@ impl<'a, Message, Renderer> Button<'a, Message, Renderer> { } } - pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); + /// 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>(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>, + ) -> Self { + self.style_sheet = style_sheet.into(); self } } From af122265f6663e429a3732ecdbbf2356688702b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 23:39:41 +0700 Subject: [PATCH 10/55] Implement `Row` in `iced_pure` --- pure/src/widget.rs | 6 ++ pure/src/widget/row.rs | 223 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 pure/src/widget/row.rs diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 9a4dffe3a1..1cbd3b78a6 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,12 +1,14 @@ mod button; mod column; mod element; +mod row; mod text; mod tree; pub use button::Button; pub use column::Column; pub use element::Element; +pub use row::Row; pub use text::Text; pub use tree::Tree; @@ -76,6 +78,10 @@ pub fn column<'a, Message, Renderer>() -> Column<'a, Message, Renderer> { Column::new() } +pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> { + Row::new() +} + pub fn button<'a, Message, Renderer>( content: impl Into>, ) -> Button<'a, Message, Renderer> { diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs new file mode 100644 index 0000000000..2581b38ace --- /dev/null +++ b/pure/src/widget/row.rs @@ -0,0 +1,223 @@ +use crate::flex; +use crate::widget::{Element, Tree, 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, Hasher, Length, Padding, Point, Rectangle, Shell, +}; + +use std::any::{self, Any}; + +pub struct Row<'a, Message, Renderer> { + spacing: u16, + padding: Padding, + width: Length, + height: Length, + align_items: Alignment, + children: Vec>, +} + +impl<'a, Message, Renderer> Row<'a, Message, Renderer> { + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + pub fn with_children( + children: Vec>, + ) -> 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>(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>, + ) -> Self { + self.children.push(child.into()); + self + } +} + +impl<'a, Message, Renderer> Widget + for Row<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + struct Marker; + any::TypeId::of::() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children(&self) -> &[Element] { + &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 hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.tag().hash(state); + self.width.hash(state); + self.height.hash(state); + self.align_items.hash(state); + self.spacing.hash(state); + self.padding.hash(state); + + for child in &self.children { + child.as_widget().hash_layout(state); + } + } +} + +impl<'a, Message, Renderer> Into> + for Row<'a, Message, Renderer> +where + Message: 'static, + Renderer: iced_native::Renderer + 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From 8b27083cdaa2ef7b749e0fd2c1a94b5606ed1c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 11 Feb 2022 23:40:24 +0700 Subject: [PATCH 11/55] Use `TypeId` of `()` for `Column` and `Row` tags in `iced_pure` --- pure/src/widget/column.rs | 3 +-- pure/src/widget/row.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 716fd714ae..ed097d3344 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -78,8 +78,7 @@ where Renderer: iced_native::Renderer, { fn tag(&self) -> any::TypeId { - struct Marker; - any::TypeId::of::() + any::TypeId::of::<()>() } fn state(&self) -> Box { diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 2581b38ace..147a0850d2 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -78,8 +78,7 @@ where Renderer: iced_native::Renderer, { fn tag(&self) -> any::TypeId { - struct Marker; - any::TypeId::of::() + any::TypeId::of::<()>() } fn state(&self) -> Box { From 182fb9446c577a6be988052a5103010e1a79addd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 14:07:04 +0700 Subject: [PATCH 12/55] Implement `Container` widget in `iced_pure` --- native/src/widget/container.rs | 58 +++++--- pure/src/widget.rs | 11 ++ pure/src/widget/container.rs | 258 +++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 22 deletions(-) create mode 100644 pure/src/widget/container.rs diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 4444732a62..90db7e33f0 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -118,6 +118,32 @@ where } } +/// Computes the layout of a [`Container`]. +pub fn layout( + 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 for Container<'a, Message, Renderer> where @@ -136,28 +162,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/pure/src/widget.rs b/pure/src/widget.rs index 1cbd3b78a6..3bf6a5aa3a 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,5 +1,6 @@ mod button; mod column; +mod container; mod element; mod row; mod text; @@ -7,6 +8,7 @@ mod tree; pub use button::Button; pub use column::Column; +pub use container::Container; pub use element::Element; pub use row::Row; pub use text::Text; @@ -74,6 +76,15 @@ pub trait Widget { } } +pub fn container<'a, Message, Renderer>( + content: impl Into>, +) -> Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + Container::new(content) +} + pub fn column<'a, Message, Renderer>() -> Column<'a, Message, Renderer> { Column::new() } diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs new file mode 100644 index 0000000000..94a6b07b2e --- /dev/null +++ b/pure/src/widget/container.rs @@ -0,0 +1,258 @@ +//! Decorate content and apply alignment. +use crate::{Element, Tree, Widget}; + +use iced_native::alignment; +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::container; +use iced_native::{ + Clipboard, Hasher, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::any::{self, Any}; +use std::hash::Hash; +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, + 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(content: T) -> Self + where + T: Into>, + { + 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>(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>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget + for Container<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children(&self) -> &[Element] { + 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 hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::().hash(state); + + self.padding.hash(state); + self.width.hash(state); + self.height.hash(state); + self.max_width.hash(state); + self.max_height.hash(state); + self.horizontal_alignment.hash(state); + self.vertical_alignment.hash(state); + + self.content.as_widget().hash_layout(state); + } +} + +impl<'a, Message, Renderer> From> + 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) + } +} From dee3dba632709f57b5573dbe28827ad481287648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 14:22:17 +0700 Subject: [PATCH 13/55] Reuse `Text` widget from `iced_native` in `iced_pure` --- examples/pure/counter/src/main.rs | 2 +- pure/src/widget.rs | 2 +- pure/src/widget/text.rs | 131 ++++-------------------------- 3 files changed, 19 insertions(+), 116 deletions(-) diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs index 4cb79a0eb0..9b5203472f 100644 --- a/examples/pure/counter/src/main.rs +++ b/examples/pure/counter/src/main.rs @@ -43,7 +43,7 @@ impl Sandbox for Counter { .padding(20) .align_items(Alignment::Center) .push(button("Increment").on_press(Message::IncrementPressed)) - .push(text(self.value).size(50)) + .push(text(self.value.to_string()).size(50)) .push(button("Decrement").on_press(Message::DecrementPressed)) .into() } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 3bf6a5aa3a..9ab656143b 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -99,7 +99,7 @@ pub fn button<'a, Message, Renderer>( Button::new(content) } -pub fn text(text: impl ToString) -> Text +pub fn text(text: impl Into) -> Text where Renderer: iced_native::text::Renderer, { diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index 5a5f360e8b..f437b48be9 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -1,92 +1,13 @@ use crate::{Element, Tree, Widget}; -use iced_native::alignment; use iced_native::layout::{self, Layout}; use iced_native::renderer; use iced_native::text; -use iced_native::{Color, Hasher, Length, Point, Rectangle, Size}; +use iced_native::{Hasher, Length, Point, Rectangle}; use std::any::{self, Any}; -pub struct Text -where - Renderer: text::Renderer, -{ - content: String, - size: Option, - color: Option, - font: Renderer::Font, - width: Length, - height: Length, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, -} - -impl Text { - /// Create a new fragment of [`Text`] with the given contents. - pub fn new(label: T) -> Self { - Text { - content: label.to_string(), - size: None, - color: None, - font: Default::default(), - width: Length::Shrink, - height: Length::Shrink, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - } - } - - /// Sets the size of the [`Text`]. - pub fn size(mut self, size: u16) -> Self { - self.size = Some(size); - self - } - - /// Sets the [`Color`] of the [`Text`]. - pub fn color>(mut self, color: C) -> Self { - self.color = Some(color.into()); - self - } - - /// Sets the [`Font`] of the [`Text`]. - /// - /// [`Font`]: Renderer::Font - pub fn font(mut self, font: impl Into) -> Self { - self.font = font.into(); - self - } - - /// Sets the width of the [`Text`] boundaries. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the height of the [`Text`] boundaries. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// Sets the [`HorizontalAlignment`] of the [`Text`]. - pub fn horizontal_alignment( - mut self, - alignment: alignment::Horizontal, - ) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the [`VerticalAlignment`] of the [`Text`]. - pub fn vertical_alignment( - mut self, - alignment: alignment::Vertical, - ) -> Self { - self.vertical_alignment = alignment; - self - } -} +pub use iced_native::widget::Text; impl Widget for Text where @@ -105,11 +26,11 @@ where } fn width(&self) -> Length { - self.width + >::width(self) } fn height(&self) -> Length { - self.height + >::height(self) } fn layout( @@ -117,18 +38,9 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - let size = self.size.unwrap_or(renderer.default_size()); - - let bounds = limits.max(); - - let (width, height) = - renderer.measure(&self.content, size, self.font.clone(), bounds); - - let size = limits.resolve(Size::new(width, height)); - - layout::Node::new(size) + >::layout( + self, renderer, limits, + ) } fn draw( @@ -137,32 +49,23 @@ where renderer: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, + cursor_position: Point, + viewport: &Rectangle, ) { - iced_native::widget::text::draw( + >::draw( + self, renderer, style, layout, - &self.content, - self.font.clone(), - self.size, - self.color, - self.horizontal_alignment, - self.vertical_alignment, - ); + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { - use std::hash::Hash; - - struct Marker; - std::any::TypeId::of::().hash(state); - - self.content.hash(state); - self.size.hash(state); - self.width.hash(state); - self.height.hash(state); + >::hash_layout( + self, state, + ) } } From 178914ec23a107cb7fa38c39be30a35d235248ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 14:26:17 +0700 Subject: [PATCH 14/55] Implement `Checkbox` in `iced_pure` --- native/src/widget/checkbox.rs | 4 +- pure/src/widget.rs | 13 ++++++ pure/src/widget/checkbox.rs | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 pure/src/widget/checkbox.rs diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index fff65a40f6..89fb39633b 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -36,7 +36,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 Message>, + on_toggle: Box Message + 'a>, label: String, width: Length, size: u16, @@ -63,7 +63,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// `Message`. pub fn new(is_checked: bool, label: impl Into, f: F) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Checkbox { is_checked, diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 9ab656143b..3488f99da8 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ mod button; +mod checkbox; mod column; mod container; mod element; @@ -7,6 +8,7 @@ mod text; mod tree; pub use button::Button; +pub use checkbox::Checkbox; pub use column::Column; pub use container::Container; pub use element::Element; @@ -105,3 +107,14 @@ where { Text::new(text) } + +pub fn checkbox<'a, Message, Renderer>( + label: impl Into, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> Checkbox<'a, Message, Renderer> +where + Renderer: iced_native::text::Renderer, +{ + Checkbox::new(is_checked, label, f) +} diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs new file mode 100644 index 0000000000..8ee2b7bb7d --- /dev/null +++ b/pure/src/widget/checkbox.rs @@ -0,0 +1,82 @@ +use crate::{Element, Tree, Widget}; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::text; +use iced_native::{Hasher, Length, Point, Rectangle}; + +use std::any::{self, Any}; + +pub use iced_native::widget::Checkbox; + +impl<'a, Message, Renderer> Widget + for Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children(&self) -> &[Element] { + &[] + } + + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + >::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + >::hash_layout( + self, state, + ) + } +} + +impl<'a, Message, Renderer> Into> + for Checkbox<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From b2670e8752eb96a4018f93b9cb8945da81a7ebff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 15:17:44 +0700 Subject: [PATCH 15/55] Implement `Scrollable` in `iced_pure` --- native/src/widget/scrollable.rs | 889 +++++++++++++++++++------------- pure/src/widget.rs | 9 + pure/src/widget/button.rs | 4 +- pure/src/widget/scrollable.rs | 268 ++++++++++ pure/src/widget/tree.rs | 18 +- 5 files changed, 810 insertions(+), 378 deletions(-) create mode 100644 pure/src/widget/scrollable.rs diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 4b005c6b20..25fd26138b 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -139,235 +139,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 { - 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 - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn width(&self) -> Length { - Widget::::width(&self.content) - } - - fn height(&self) -> Length { - self.height - } +/// Computes the layout of a [`Scrollable`]. +pub fn layout( + 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::::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` 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( + 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 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` 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 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; - 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; + 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 +342,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( + 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 { + 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( + state: &State, + on_scroll: &Option 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 + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + Widget::::width(&self.content) + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::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 +674,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 +700,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 hash_layout(&self, state: &mut Hasher) { diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 3488f99da8..02bf3a85bd 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -4,6 +4,7 @@ mod column; mod container; mod element; mod row; +mod scrollable; mod text; mod tree; @@ -13,6 +14,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use row::Row; +pub use scrollable::Scrollable; pub use text::Text; pub use tree::Tree; @@ -95,6 +97,13 @@ pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> { Row::new() } +pub fn scrollable<'a, Message, Renderer>() -> Scrollable<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + Scrollable::new() +} + pub fn button<'a, Message, Renderer>( content: impl Into>, ) -> Button<'a, Message, Renderer> { diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index b9561b0941..d0f9e53eb7 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -152,7 +152,7 @@ where cursor_position, shell, &self.on_press, - || tree.state_mut::(), + || tree.state.downcast_mut::(), ) } @@ -174,7 +174,7 @@ where cursor_position, self.on_press.is_some(), self.style_sheet.as_ref(), - || tree.state::(), + || tree.state.downcast_ref::(), ); self.content.as_widget().draw( diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs new file mode 100644 index 0000000000..69acabb718 --- /dev/null +++ b/pure/src/widget/scrollable.rs @@ -0,0 +1,268 @@ +use crate::widget::{Column, 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::{ + Alignment, Clipboard, Hasher, Length, Padding, Point, Rectangle, Shell, +}; + +pub use iced_style::scrollable::StyleSheet; + +use std::any::{self, Any}; + +/// 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, + content: Column<'a, Message, Renderer>, + on_scroll: Option Message>>, + style_sheet: Box, +} + +impl<'a, Message, Renderer: iced_native::Renderer> + Scrollable<'a, Message, Renderer> +{ + /// Creates a new [`Scrollable`] with the given [`State`]. + pub fn new() -> Self { + Scrollable { + height: Length::Shrink, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, + content: Column::new(), + on_scroll: None, + style_sheet: Default::default(), + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in Iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, units: u16) -> Self { + self.content = self.content.spacing(units); + self + } + + /// Sets the [`Padding`] of the [`Scrollable`]. + pub fn padding>(mut self, padding: P) -> Self { + self.content = self.content.padding(padding); + self + } + + /// Sets the width of the [`Scrollable`]. + pub fn width(mut self, width: Length) -> Self { + self.content = self.content.width(width); + self + } + + /// Sets the height of the [`Scrollable`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the horizontal alignment of the contents of the [`Scrollable`] . + pub fn align_items(mut self, align_items: Alignment) -> Self { + self.content = self.content.align_items(align_items); + 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>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } + + /// Adds an element to the [`Scrollable`]. + pub fn push(mut self, child: E) -> Self + where + E: Into>, + { + self.content = self.content.push(child); + self + } +} + +impl<'a, Message, Renderer> Widget + for Scrollable<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::() + } + + fn state(&self) -> Box { + Box::new(scrollable::State::new()) + } + + fn children(&self) -> &[Element] { + self.content.children() + } + + fn width(&self) -> Length { + Widget::::width(&self.content) + } + + fn height(&self) -> Length { + self.height + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.tag().hash(state); + self.height.hash(state); + self.content.hash_layout(state) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + scrollable::layout( + renderer, + limits, + Widget::::width(self), + self.height, + |renderer, limits| self.content.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::(), + 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( + &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::(), + 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( + &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::(), + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } +} diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 1ab6d80b88..98e976add1 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -4,7 +4,7 @@ use std::any::{self, Any}; pub struct Tree { pub tag: any::TypeId, - pub state: Box, + pub state: State, pub children: Vec, } @@ -12,7 +12,7 @@ impl Tree { pub fn empty() -> Self { Self { tag: any::TypeId::of::<()>(), - state: Box::new(()), + state: State(Box::new(())), children: Vec::new(), } } @@ -22,7 +22,7 @@ impl Tree { ) -> Self { Self { tag: element.as_widget().tag(), - state: element.as_widget().state(), + state: State(element.as_widget().state()), children: element .as_widget() .children() @@ -58,18 +58,22 @@ impl Tree { *self = Self::new(new); } } +} + +pub struct State(Box); - pub fn state(&self) -> &T +impl State { + pub fn downcast_ref(&self) -> &T where T: 'static, { - self.state.downcast_ref().expect("Downcast widget state") + self.0.downcast_ref().expect("Downcast widget state") } - pub fn state_mut(&mut self) -> &mut T + pub fn downcast_mut(&mut self) -> &mut T where T: 'static, { - self.state.downcast_mut().expect("Downcast widget state") + self.0.downcast_mut().expect("Downcast widget state") } } From e3108494e5886c34312184292ec05dddeb8bf3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 16:11:22 +0700 Subject: [PATCH 16/55] Implement `TextInput` in `iced_pure` --- native/src/widget/text_input.rs | 925 ++++++++++++++++---------------- pure/src/widget.rs | 14 + pure/src/widget/text_input.rs | 239 +++++++++ 3 files changed, 730 insertions(+), 448 deletions(-) create mode 100644 pure/src/widget/text_input.rs diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 03adeb12b8..83ce75f40d 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -24,8 +24,6 @@ use crate::{ Rectangle, 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, - on_change: Box Message>, + on_change: Box Message + 'a>, on_submit: Option, style_sheet: Box, } @@ -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>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -166,520 +156,313 @@ where } } -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +/// Computes the layout of a [`TextInput`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option, +) -> layout::Node where Renderer: text::Renderer, { - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. - pub fn draw( - &self, - renderer: &mut Renderer, - layout: Layout<'_>, - 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 text_size = size.unwrap_or(renderer.default_size()); - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); + let limits = limits + .pad(padding) + .width(width) + .height(Length::Units(text_size)); - let is_mouse_over = bounds.contains(cursor_position); + let mut text = layout::Node::new(limits.resolve(Size::ZERO)); + text.move_to(Point::new(padding.left.into(), padding.top.into())); - 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); - } - } + layout::Node::with_children(text.size().pad(padding), vec![text]) } -impl<'a, Message, Renderer> Widget - for TextInput<'a, Message, Renderer> +/// 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, + font: &Renderer::Font, + is_secure: bool, + on_change: &dyn Fn(String) -> Message, + on_submit: &Option, + state: impl FnOnce() -> &'a mut State, +) -> event::Status 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); - - self.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, - self.state.last_click, - ); - - 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, - ) + 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 +476,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 +509,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 +531,261 @@ 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 +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +pub fn draw( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: &Value, + placeholder: &str, + size: Option, + 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); - event::Status::Ignored + 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() + } +} + +/// Hashes the layout attributes of a [`TextInput`]. +pub fn hash_layout( + state: &mut Hasher, + width: Length, + padding: Padding, + size: Option, +) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + width.hash(state); + padding.hash(state); + size.hash(state); +} + +impl<'a, Message, Renderer> Widget + 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 +795,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( @@ -781,18 +806,22 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - self.draw(renderer, layout, cursor_position, None) + draw( + renderer, + layout, + cursor_position, + &self.state, + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style_sheet.as_ref(), + ) } fn hash_layout(&self, state: &mut Hasher) { - use std::{any::TypeId, hash::Hash}; - struct Marker; - TypeId::of::().hash(state); - - self.width.hash(state); - self.max_width.hash(state); - self.padding.hash(state); - self.size.hash(state); + hash_layout(state, self.width, self.padding, self.size) } } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 02bf3a85bd..8df6442675 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -6,6 +6,7 @@ mod element; mod row; mod scrollable; mod text; +mod text_input; mod tree; pub use button::Button; @@ -16,6 +17,7 @@ pub use element::Element; pub use row::Row; pub use scrollable::Scrollable; pub use text::Text; +pub use text_input::TextInput; pub use tree::Tree; use iced_native::event::{self, Event}; @@ -127,3 +129,15 @@ where { Checkbox::new(is_checked, label, f) } + +pub fn text_input<'a, Message, Renderer>( + placeholder: &str, + value: &str, + on_change: impl Fn(String) -> Message + 'a, +) -> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, +{ + TextInput::new(placeholder, value, on_change) +} diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs new file mode 100644 index 0000000000..06ab291012 --- /dev/null +++ b/pure/src/widget/text_input.rs @@ -0,0 +1,239 @@ +use crate::widget::{Element, Tree, 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, Hasher, Length, Padding, Point, Rectangle, Shell, +}; + +pub use iced_style::text_input::StyleSheet; + +use std::any::{self, Any}; + +/// 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); +/// ``` +/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true) +#[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, + on_change: Box Message + 'a>, + on_submit: Option, + style_sheet: Box, +} + +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(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>(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>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, Message, Renderer> Widget + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::() + } + + fn state(&self) -> Box { + Box::new(text_input::State::new()) + } + + fn children(&self) -> &[Element] { + &[] + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn hash_layout(&self, state: &mut Hasher) { + text_input::hash_layout(state, self.width, self.padding, self.size); + } + + 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::(), + ) + } + + 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::(), + &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) + } +} From bd22cc0bc0f7551d29cf2acd22520f4a906f253c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 17:21:28 +0700 Subject: [PATCH 17/55] Implement pure version of `todos` example :tada: The `Widget` trait in `iced_pure` needed to change a bit to make the implementation of `Element::map` possible. Specifically, the `children` method has been split into `diff` and `children_state`. --- Cargo.toml | 1 + examples/pure/todos/Cargo.toml | 19 + examples/pure/todos/README.md | 20 ++ examples/pure/todos/index.html | 12 + examples/pure/todos/src/main.rs | 599 ++++++++++++++++++++++++++++++++ pure/src/lib.rs | 3 +- pure/src/widget.rs | 10 +- pure/src/widget/button.rs | 8 +- pure/src/widget/checkbox.rs | 6 +- pure/src/widget/column.rs | 8 +- pure/src/widget/container.rs | 8 +- pure/src/widget/element.rs | 149 +++++++- pure/src/widget/row.rs | 8 +- pure/src/widget/scrollable.rs | 86 ++--- pure/src/widget/text.rs | 6 +- pure/src/widget/text_input.rs | 19 +- pure/src/widget/tree.rs | 44 +-- src/pure.rs | 5 +- 18 files changed, 916 insertions(+), 95 deletions(-) create mode 100644 examples/pure/todos/Cargo.toml create mode 100644 examples/pure/todos/README.md create mode 100644 examples/pure/todos/index.html create mode 100644 examples/pure/todos/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 1527c3732a..11966dd094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ members = [ "examples/tour", "examples/url_handler", "examples/pure/counter", + "examples/pure/todos", "examples/websocket", ] diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml new file mode 100644 index 0000000000..fa14ff6ade --- /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 "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["async-std", "debug", "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/README.md b/examples/pure/todos/README.md new file mode 100644 index 0000000000..9c2598b95e --- /dev/null +++ b/examples/pure/todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + + + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ diff --git a/examples/pure/todos/index.html b/examples/pure/todos/index.html new file mode 100644 index 0000000000..ee5570fb9e --- /dev/null +++ b/examples/pure/todos/index.html @@ -0,0 +1,12 @@ + + + + + + Todos - Iced + + + + + + diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs new file mode 100644 index 0000000000..9acc9fcc67 --- /dev/null +++ b/examples/pure/todos/src/main.rs @@ -0,0 +1,599 @@ +use iced::alignment::{self, Alignment}; +use iced::pure::widget::{ + button, checkbox, column, container, row, scrollable, text, text_input, +}; +use iced::pure::{Application, Element, Text}; +use iced::{Command, Font, Length, Settings}; +use serde::{Deserialize, Serialize}; + +pub fn main() -> iced::Result { + Todos::run(Settings::default()) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + input_value: String, + filter: Filter, + tasks: Vec, + dirty: bool, + saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Result), + 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) { + ( + 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 { + 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 { + 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) + .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 { + 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 { + 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, +} + +#[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 { + 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 { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + 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/pure/src/lib.rs b/pure/src/lib.rs index a179a84b05..07f068cce1 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -53,7 +53,8 @@ impl State { impl<'a, Message, Renderer> iced_native::Widget for Pure<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Message: 'a, + Renderer: iced_native::Renderer + 'a, { fn width(&self) -> Length { self.element.as_widget().width() diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 8df6442675..302a057a43 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -33,7 +33,9 @@ pub trait Widget { fn state(&self) -> Box; - fn children(&self) -> &[Element]; + fn diff(&self, tree: &mut Tree); + + fn children_state(&self) -> Vec; fn width(&self) -> Length; @@ -99,11 +101,13 @@ pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> { Row::new() } -pub fn scrollable<'a, Message, Renderer>() -> Scrollable<'a, Message, Renderer> +pub fn scrollable<'a, Message, Renderer>( + content: impl Into>, +) -> Scrollable<'a, Message, Renderer> where Renderer: iced_native::Renderer, { - Scrollable::new() + Scrollable::new(content) } pub fn button<'a, Message, Renderer>( diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index d0f9e53eb7..6dc1016cee 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -85,8 +85,12 @@ where Box::new(State::new()) } - fn children(&self) -> &[Element] { - std::slice::from_ref(&self.content) + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn children_state(&self) -> Vec { + vec![Tree::new(&self.content)] } fn width(&self) -> Length { diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs index 8ee2b7bb7d..1cfe89a82d 100644 --- a/pure/src/widget/checkbox.rs +++ b/pure/src/widget/checkbox.rs @@ -22,8 +22,10 @@ where Box::new(()) } - fn children(&self) -> &[Element] { - &[] + fn diff(&self, _tree: &mut Tree) {} + + fn children_state(&self) -> Vec { + Vec::new() } fn width(&self) -> Length { diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index ed097d3344..68d3c4b45e 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -85,8 +85,12 @@ where Box::new(()) } - fn children(&self) -> &[Element] { - &self.children + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn children_state(&self) -> Vec { + self.children.iter().map(Tree::new).collect() } fn width(&self) -> Length { diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index 94a6b07b2e..85ea80391d 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -132,8 +132,12 @@ where Box::new(()) } - fn children(&self) -> &[Element] { - std::slice::from_ref(&self.content) + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn children_state(&self) -> Vec { + vec![Tree::new(&self.content)] } fn width(&self) -> Length { diff --git a/pure/src/widget/element.rs b/pure/src/widget/element.rs index aedf597383..2a137d405e 100644 --- a/pure/src/widget/element.rs +++ b/pure/src/widget/element.rs @@ -1,4 +1,12 @@ -use crate::Widget; +use crate::widget::{Tree, 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, Hasher, Length, Point, Rectangle, Shell}; + +use std::any::{self, Any}; pub struct Element<'a, Message, Renderer> { widget: Box + 'a>, @@ -18,4 +26,143 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> { pub fn as_widget_mut(&mut self) -> &mut dyn Widget { self.widget.as_mut() } + + pub fn map( + 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 + 'a>, + mapper: Box B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + widget: Box + '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 for Map<'a, A, B, Renderer> +where + Renderer: iced_native::Renderer + 'a, + A: 'a, + B: 'a, +{ + fn tag(&self) -> any::TypeId { + self.widget.tag() + } + + fn state(&self) -> Box { + self.widget.state() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + + fn children_state(&self) -> Vec { + self.widget.children_state() + } + + 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, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + self.widget.hash_layout(state); + } } diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 147a0850d2..ec7e144ce1 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -85,8 +85,12 @@ where Box::new(()) } - fn children(&self) -> &[Element] { - &self.children + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + + fn children_state(&self) -> Vec { + self.children.iter().map(Tree::new).collect() } fn width(&self) -> Length { diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 69acabb718..badc9fc249 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -1,4 +1,4 @@ -use crate::widget::{Column, Tree}; +use crate::widget::Tree; use crate::{Element, Widget}; use iced_native::event::{self, Event}; @@ -6,9 +6,7 @@ use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::renderer; use iced_native::widget::scrollable; -use iced_native::{ - Alignment, Clipboard, Hasher, Length, Padding, Point, Rectangle, Shell, -}; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; pub use iced_style::scrollable::StyleSheet; @@ -22,61 +20,33 @@ pub struct Scrollable<'a, Message, Renderer> { scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - content: Column<'a, Message, Renderer>, on_scroll: Option Message>>, style_sheet: Box, + content: Element<'a, Message, Renderer>, } impl<'a, Message, Renderer: iced_native::Renderer> Scrollable<'a, Message, Renderer> { - /// Creates a new [`Scrollable`] with the given [`State`]. - pub fn new() -> Self { + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into>) -> Self { Scrollable { height: Length::Shrink, scrollbar_width: 10, scrollbar_margin: 0, scroller_width: 10, - content: Column::new(), on_scroll: None, style_sheet: Default::default(), + content: content.into(), } } - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in Iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, units: u16) -> Self { - self.content = self.content.spacing(units); - self - } - - /// Sets the [`Padding`] of the [`Scrollable`]. - pub fn padding>(mut self, padding: P) -> Self { - self.content = self.content.padding(padding); - self - } - - /// Sets the width of the [`Scrollable`]. - pub fn width(mut self, width: Length) -> Self { - self.content = self.content.width(width); - self - } - /// Sets the height of the [`Scrollable`]. pub fn height(mut self, height: Length) -> Self { self.height = height; self } - /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - pub fn align_items(mut self, align_items: Alignment) -> Self { - self.content = self.content.align_items(align_items); - 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 { @@ -115,15 +85,6 @@ impl<'a, Message, Renderer: iced_native::Renderer> self.style_sheet = style_sheet.into(); self } - - /// Adds an element to the [`Scrollable`]. - pub fn push(mut self, child: E) -> Self - where - E: Into>, - { - self.content = self.content.push(child); - self - } } impl<'a, Message, Renderer> Widget @@ -139,12 +100,16 @@ where Box::new(scrollable::State::new()) } - fn children(&self) -> &[Element] { - self.content.children() + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn children_state(&self) -> Vec { + vec![Tree::new(&self.content)] } fn width(&self) -> Length { - Widget::::width(&self.content) + self.content.as_widget().width() } fn height(&self) -> Length { @@ -156,7 +121,7 @@ where self.tag().hash(state); self.height.hash(state); - self.content.hash_layout(state) + self.content.as_widget().hash_layout(state) } fn layout( @@ -169,7 +134,9 @@ where limits, Widget::::width(self), self.height, - |renderer, limits| self.content.layout(renderer, limits), + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, ) } @@ -195,7 +162,7 @@ where self.scroller_width, &self.on_scroll, |event, layout, cursor_position, clipboard, shell| { - self.content.on_event( + self.content.as_widget_mut().on_event( &mut tree.children[0], event, layout, @@ -227,7 +194,7 @@ where self.scroller_width, self.style_sheet.as_ref(), |renderer, layout, cursor_position, viewport| { - self.content.draw( + self.content.as_widget().draw( &tree.children[0], renderer, style, @@ -255,7 +222,7 @@ where self.scrollbar_margin, self.scroller_width, |layout, cursor_position, viewport| { - self.content.mouse_interaction( + self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor_position, @@ -266,3 +233,16 @@ where ) } } + +impl<'a, Message, Renderer> From> + 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/text.rs b/pure/src/widget/text.rs index f437b48be9..8f157ea0fd 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -21,8 +21,10 @@ where Box::new(()) } - fn children(&self) -> &[Element] { - &[] + fn diff(&self, _tree: &mut Tree) {} + + fn children_state(&self) -> Vec { + Vec::new() } fn width(&self) -> Length { diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs index 06ab291012..e18a2bf05c 100644 --- a/pure/src/widget/text_input.rs +++ b/pure/src/widget/text_input.rs @@ -146,8 +146,10 @@ where Box::new(text_input::State::new()) } - fn children(&self) -> &[Element] { - &[] + fn diff(&self, _tree: &mut Tree) {} + + fn children_state(&self) -> Vec { + Vec::new() } fn width(&self) -> Length { @@ -237,3 +239,16 @@ where text_input::mouse_interaction(layout, cursor_position) } } + +impl<'a, Message, Renderer> From> + 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/tree.rs b/pure/src/widget/tree.rs index 98e976add1..3a5f443316 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -23,12 +23,7 @@ impl Tree { Self { tag: element.as_widget().tag(), state: State(element.as_widget().state()), - children: element - .as_widget() - .children() - .iter() - .map(Self::new) - .collect(), + children: element.as_widget().children_state(), } } @@ -37,25 +32,30 @@ impl Tree { new: &Element<'_, Message, Renderer>, ) { if self.tag == new.as_widget().tag() { - let new_children = new.as_widget().children(); + new.as_widget().diff(self) + } else { + *self = Self::new(new); + } + } - if self.children.len() > new_children.len() { - self.children.truncate(new_children.len()); - } + pub fn diff_children( + &mut self, + new_children: &[Element<'_, Message, Renderer>], + ) { + 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()) - { - child_state.diff(new); - } + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + child_state.diff(new); + } - if self.children.len() < new_children.len() { - self.children.extend( - new_children[self.children.len()..].iter().map(Self::new), - ); - } - } else { - *self = Self::new(new); + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(Self::new), + ); } } } diff --git a/src/pure.rs b/src/pure.rs index 7fe2bc59d0..29495c07fa 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -17,12 +17,15 @@ //! [the original widgets]: crate::widget //! [`button::State`]: crate::widget::button::State //! [impure `Application`]: crate::Application -pub use iced_pure::{Element as _, *}; +pub use iced_pure::{Element as _, Text as _, *}; /// A generic, pure [`Widget`]. pub type Element<'a, Message> = iced_pure::Element<'a, Message, crate::Renderer>; +/// A pure text widget. +pub type Text = iced_pure::Text; + mod application; mod sandbox; From 4c61601aa3fe7f6735e27c27d379090d777b56f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 17:26:49 +0700 Subject: [PATCH 18/55] Implement missing `on_event` and `mouse_interaction` for `Checkbox` in `iced_pure` --- examples/pure/todos/Cargo.toml | 2 +- pure/src/widget/checkbox.rs | 42 +++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml index fa14ff6ade..217179e83d 100644 --- a/examples/pure/todos/Cargo.toml +++ b/examples/pure/todos/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../../..", features = ["async-std", "debug", "pure"] } +iced = { path = "../../..", features = ["async-std", "debug", "default_system_font", "pure"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs index 1cfe89a82d..5352fad3f7 100644 --- a/pure/src/widget/checkbox.rs +++ b/pure/src/widget/checkbox.rs @@ -1,9 +1,11 @@ use crate::{Element, Tree, 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::{Hasher, Length, Point, Rectangle}; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; use std::any::{self, Any}; @@ -46,6 +48,27 @@ where ) } + 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 { + >::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + fn draw( &self, _tree: &Tree, @@ -65,6 +88,23 @@ where ) } + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + >::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } + fn hash_layout(&self, state: &mut Hasher) { >::hash_layout( self, state, From 09c96a6d8123a62411e2c461a018c3900dec71cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 12 Feb 2022 18:02:29 +0700 Subject: [PATCH 19/55] Add `max_width` to `Column` in `iced_pure` --- examples/pure/todos/src/main.rs | 10 +++++++++- pure/src/widget/column.rs | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs index 9acc9fcc67..e993c598bd 100644 --- a/examples/pure/todos/src/main.rs +++ b/examples/pure/todos/src/main.rs @@ -3,11 +3,18 @@ use iced::pure::widget::{ button, checkbox, column, container, row, scrollable, text, text_input, }; use iced::pure::{Application, Element, Text}; +use iced::window; use iced::{Command, Font, Length, Settings}; use serde::{Deserialize, Serialize}; pub fn main() -> iced::Result { - Todos::run(Settings::default()) + Todos::run(Settings { + window: window::Settings { + size: (500, 800), + ..window::Settings::default() + }, + ..Settings::default() + }) } #[derive(Debug)] @@ -184,6 +191,7 @@ impl Application for Todos { let content = column() .spacing(20) + .max_width(800) .push(title) .push(input) .push(controls) diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 68d3c4b45e..a9d7246e45 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -10,12 +10,14 @@ use iced_native::{ }; use std::any::{self, Any}; +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>, } @@ -33,6 +35,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, + max_width: u32::MAX, align_items: Alignment::Start, children, } @@ -58,6 +61,12 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { 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 @@ -106,7 +115,10 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); flex::resolve( flex::Axis::Vertical, @@ -204,6 +216,7 @@ where self.tag().hash(state); self.width.hash(state); self.height.hash(state); + self.max_width.hash(state); self.align_items.hash(state); self.spacing.hash(state); self.padding.hash(state); From 45455be45000c0d41d18eced1b62eab049c5e9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 16:51:31 +0700 Subject: [PATCH 20/55] Implement `Image` in `iced_pure` --- native/src/widget/image.rs | 73 ++++++++++++++++++++++++------------- pure/src/widget.rs | 7 ++++ pure/src/widget/image.rs | 74 ++++++++++++++++++++++++++++++++++++++ src/pure.rs | 5 ++- 4 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 pure/src/widget/image.rs diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index b8fb662e1d..5e03bf99fb 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -51,6 +51,53 @@ impl Image { } } +/// Computes the layout of an [`Image`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + handle: &Handle, + width: Length, + height: Length, +) -> layout::Node +where + Renderer: image::Renderer, +{ + let (original_width, original_height) = renderer.dimensions(handle); + + let mut size = limits + .width(width) + .height(height) + .resolve(Size::new(original_width as f32, original_height as f32)); + + let aspect_ratio = original_width as f32 / original_height as f32; + let viewport_aspect_ratio = size.width / size.height; + + if viewport_aspect_ratio > aspect_ratio { + size.width = + original_width as f32 * size.height / original_height as f32; + } else { + size.height = + original_height as f32 * size.width / original_width as f32; + } + + layout::Node::new(size) +} + +/// Hashes the layout attributes of an [`Image`]. +pub fn hash_layout( + state: &mut Hasher, + handle: &Handle, + width: Length, + height: Length, +) { + struct Marker; + std::any::TypeId::of::().hash(state); + + handle.hash(state); + width.hash(state); + height.hash(state); +} + impl Widget for Image where Renderer: image::Renderer, @@ -69,24 +116,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let (width, height) = renderer.dimensions(&self.handle); - - let aspect_ratio = width as f32 / height as f32; - - let mut size = limits - .width(self.width) - .height(self.height) - .resolve(Size::new(width as f32, height as f32)); - - let viewport_aspect_ratio = size.width / size.height; - - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; - } else { - size.height = height as f32 * size.width / width as f32; - } - - layout::Node::new(size) + layout(renderer, limits, &self.handle, self.width, self.height) } fn draw( @@ -101,12 +131,7 @@ where } fn hash_layout(&self, state: &mut Hasher) { - struct Marker; - std::any::TypeId::of::().hash(state); - - self.handle.hash(state); - self.width.hash(state); - self.height.hash(state); + hash_layout(state, &self.handle, self.width, self.height) } } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 302a057a43..93298c6143 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,3 +1,5 @@ +pub mod image; + mod button; mod checkbox; mod column; @@ -14,6 +16,7 @@ pub use checkbox::Checkbox; pub use column::Column; pub use container::Container; pub use element::Element; +pub use image::Image; pub use row::Row; pub use scrollable::Scrollable; pub use text::Text; @@ -145,3 +148,7 @@ where { TextInput::new(placeholder, value, on_change) } + +pub fn image(handle: Handle) -> Image { + Image::new(handle) +} diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs new file mode 100644 index 0000000000..b33dad2bee --- /dev/null +++ b/pure/src/widget/image.rs @@ -0,0 +1,74 @@ +use crate::widget::{Tree, Widget}; + +use iced_native::layout::{self, Layout}; +use iced_native::renderer; +use iced_native::widget::image; +use iced_native::{Hasher, Length, Point, Rectangle}; + +use std::any::{self, Any}; +use std::hash::Hash; + +pub use image::Image; + +impl Widget for Image +where + Handle: Clone + Hash, + Renderer: iced_native::image::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children_state(&self) -> Vec { + Vec::new() + } + + fn diff(&self, _tree: &mut Tree) {} + + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + >::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + >::hash_layout( + self, state, + ) + } +} diff --git a/src/pure.rs b/src/pure.rs index 29495c07fa..712bd31f65 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -17,7 +17,7 @@ //! [the original widgets]: crate::widget //! [`button::State`]: crate::widget::button::State //! [impure `Application`]: crate::Application -pub use iced_pure::{Element as _, Text as _, *}; +pub use iced_pure::{Element as _, Image as _, Text as _, *}; /// A generic, pure [`Widget`]. pub type Element<'a, Message> = @@ -26,6 +26,9 @@ pub type Element<'a, Message> = /// A pure text widget. pub type Text = iced_pure::Text; +/// A pure image widget. +pub type Image = iced_pure::Image; + mod application; mod sandbox; From 3f1a45ca47dc086a5c4e45867d3f9c63a4e7ba19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 17:20:10 +0700 Subject: [PATCH 21/55] Implement `Slider` in `iced_pure` --- native/src/widget/slider.rs | 397 +++++++++++++++++++++--------------- pure/Cargo.toml | 1 + pure/src/widget.rs | 19 +- pure/src/widget/slider.rs | 253 +++++++++++++++++++++++ 4 files changed, 503 insertions(+), 167 deletions(-) create mode 100644 pure/src/widget/slider.rs diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 917bfc14e3..e41e895dbf 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -143,6 +143,215 @@ where } } +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option, +) -> event::Status +where + T: Copy + Into + 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( + renderer: &mut impl crate::Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive, + style_sheet: &dyn StyleSheet, +) where + T: Into + 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() + } +} + +/// Hashes the layout of a [`Slider`]. +pub fn hash_layout(state: &mut Hasher, width: Length) { + struct Marker; + std::any::TypeId::of::().hash(state); + + width.hash(state); +} + /// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -193,73 +402,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( @@ -270,90 +424,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( @@ -363,23 +442,11 @@ 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) } fn hash_layout(&self, state: &mut Hasher) { - struct Marker; - std::any::TypeId::of::().hash(state); - - self.width.hash(state); + hash_layout(state, self.width) } } diff --git a/pure/Cargo.toml b/pure/Cargo.toml index bdfe43ae79..317dccdf90 100644 --- a/pure/Cargo.toml +++ b/pure/Cargo.toml @@ -6,3 +6,4 @@ 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/widget.rs b/pure/src/widget.rs index 93298c6143..009741a811 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -7,6 +7,7 @@ mod container; mod element; mod row; mod scrollable; +mod slider; mod text; mod text_input; mod tree; @@ -19,6 +20,7 @@ pub use element::Element; pub use image::Image; pub use row::Row; pub use scrollable::Scrollable; +pub use slider::Slider; pub use text::Text; pub use text_input::TextInput; pub use tree::Tree; @@ -36,8 +38,6 @@ pub trait Widget { fn state(&self) -> Box; - fn diff(&self, tree: &mut Tree); - fn children_state(&self) -> Vec; fn width(&self) -> Length; @@ -62,6 +62,8 @@ pub trait Widget { viewport: &Rectangle, ); + fn diff(&self, _tree: &mut Tree) {} + fn mouse_interaction( &self, _state: &Tree, @@ -149,6 +151,19 @@ where TextInput::new(placeholder, value, on_change) } +pub fn slider<'a, Message, Renderer, T>( + range: std::ops::RangeInclusive, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> Slider<'a, T, Message> +where + Message: Clone, + Renderer: iced_native::Renderer, + T: Copy + From + std::cmp::PartialOrd, +{ + Slider::new(range, value, on_change) +} + pub fn image(handle: Handle) -> Image { Image::new(handle) } diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs new file mode 100644 index 0000000000..f659c2ed0b --- /dev/null +++ b/pure/src/widget/slider.rs @@ -0,0 +1,253 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::{Element, Tree, 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, Hasher, Layout, Length, Point, Rectangle, Shell, Size, +}; + +use std::any::{self, Any}; +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); +/// ``` +/// +/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) +#[allow(missing_debug_implementations)] +pub struct Slider<'a, T, Message> { + range: RangeInclusive, + step: T, + value: T, + on_change: Box Message + 'a>, + on_release: Option, + width: Length, + height: u16, + style_sheet: Box, +} + +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From + 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(range: RangeInclusive, 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>, + ) -> 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 + for Slider<'a, T, Message> +where + T: Copy + Into + num_traits::FromPrimitive, + Message: Clone, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::() + } + + fn state(&self) -> Box { + Box::new(slider::State::new()) + } + + fn children_state(&self) -> Vec { + Vec::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::(), + &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::(), + 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::(), + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + slider::hash_layout(state, self.width) + } +} + +impl<'a, T, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into + 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) + } +} From 0fec0a2b77b6b9447117f2fea81c700a25fbca6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 19:01:09 +0700 Subject: [PATCH 22/55] Implement `Toggler` in `iced_pure` --- pure/src/widget.rs | 2 + pure/src/widget/image.rs | 13 ++++ pure/src/widget/toggler.rs | 123 +++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 pure/src/widget/toggler.rs diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 009741a811..1c0633a682 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -10,6 +10,7 @@ mod scrollable; mod slider; mod text; mod text_input; +mod toggler; mod tree; pub use button::Button; @@ -23,6 +24,7 @@ pub use scrollable::Scrollable; pub use slider::Slider; pub use text::Text; pub use text_input::TextInput; +pub use toggler::Toggler; pub use tree::Tree; use iced_native::event::{self, Event}; diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs index b33dad2bee..51a24ed170 100644 --- a/pure/src/widget/image.rs +++ b/pure/src/widget/image.rs @@ -1,4 +1,5 @@ use crate::widget::{Tree, Widget}; +use crate::Element; use iced_native::layout::{self, Layout}; use iced_native::renderer; @@ -72,3 +73,15 @@ where ) } } + +impl<'a, Message, Renderer, Handle> Into> + for Image +where + Message: Clone + 'a, + Renderer: iced_native::image::Renderer + 'a, + Handle: Clone + Hash + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/toggler.rs b/pure/src/widget/toggler.rs new file mode 100644 index 0000000000..ec86fff0f0 --- /dev/null +++ b/pure/src/widget/toggler.rs @@ -0,0 +1,123 @@ +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, Hasher, Length, Point, Rectangle, Shell}; + +use std::any::{self, Any}; + +pub use iced_native::widget::toggler::{Style, StyleSheet, Toggler}; + +impl<'a, Message, Renderer> Widget + for Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn children_state(&self) -> Vec { + Vec::new() + } + + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn hash_layout(&self, state: &mut Hasher) { + >::hash_layout( + self, state, + ) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + >::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 { + >::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 { + >::on_event( + self, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } +} + +impl<'a, Message, Renderer> Into> + for Toggler<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From 98750782f344943c58e00738867b741c53ecdfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 19:23:15 +0700 Subject: [PATCH 23/55] Introduce lifetime to `on_change` handler for `Toggler` --- native/src/widget/toggler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 002e0a4ffd..8330fc0cb0 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -33,7 +33,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 Message>, + on_toggle: Box Message + 'a>, label: Option, width: Length, size: u16, @@ -62,7 +62,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, From e50e639b0edc6eee41754e6faee45936fedeebd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 19:23:51 +0700 Subject: [PATCH 24/55] Expose additional helpers in `iced::pure` --- pure/src/widget.rs | 18 ++++++++++++++---- src/pure.rs | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 1c0633a682..fc86e1c2e7 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -141,6 +141,17 @@ where Checkbox::new(is_checked, label, f) } +pub fn toggler<'a, Message, Renderer>( + label: impl Into>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> Toggler<'a, Message, Renderer> +where + Renderer: iced_native::text::Renderer, +{ + Toggler::new(is_checked, label, f) +} + pub fn text_input<'a, Message, Renderer>( placeholder: &str, value: &str, @@ -153,19 +164,18 @@ where TextInput::new(placeholder, value, on_change) } -pub fn slider<'a, Message, Renderer, T>( +pub fn slider<'a, Message, T>( range: std::ops::RangeInclusive, value: T, on_change: impl Fn(T) -> Message + 'a, ) -> Slider<'a, T, Message> where Message: Clone, - Renderer: iced_native::Renderer, T: Copy + From + std::cmp::PartialOrd, { Slider::new(range, value, on_change) } -pub fn image(handle: Handle) -> Image { - Image::new(handle) +pub fn image(handle: impl Into) -> Image { + Image::new(handle.into()) } diff --git a/src/pure.rs b/src/pure.rs index 712bd31f65..5cab5fd9c5 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -17,12 +17,27 @@ //! [the original widgets]: crate::widget //! [`button::State`]: crate::widget::button::State //! [impure `Application`]: crate::Application -pub use iced_pure::{Element as _, Image as _, Text as _, *}; +pub use iced_pure::{ + Button as _, Column as _, Element as _, Image as _, Row as _, Text as _, *, +}; /// A generic, pure [`Widget`]. pub type Element<'a, Message> = iced_pure::Element<'a, Message, crate::Renderer>; +/// A pure container widget. +pub type Container<'a, Message> = + iced_pure::Container<'a, Message, crate::Renderer>; + +/// A pure column widget. +pub type Column<'a, Message> = iced_pure::Column<'a, Message, crate::Renderer>; + +/// A pure row widget. +pub type Row<'a, Message> = iced_pure::Row<'a, Message, crate::Renderer>; + +/// A pure button widget. +pub type Button<'a, Message> = iced_pure::Button<'a, Message, crate::Renderer>; + /// A pure text widget. pub type Text = iced_pure::Text; From 53f382043235d7ab9eae9b0882de3e8c77cc0d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 22:13:47 +0700 Subject: [PATCH 25/55] Implement `Radio` in `iced_pure` --- native/src/widget/radio.rs | 2 +- pure/src/widget.rs | 16 +++++ pure/src/widget/radio.rs | 125 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 pure/src/widget/radio.rs diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index c49927646d..862afd29e3 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -81,7 +81,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/pure/src/widget.rs b/pure/src/widget.rs index fc86e1c2e7..7b5fc0bc1d 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -5,6 +5,7 @@ mod checkbox; mod column; mod container; mod element; +mod radio; mod row; mod scrollable; mod slider; @@ -19,6 +20,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use image::Image; +pub use radio::Radio; pub use row::Row; pub use scrollable::Scrollable; pub use slider::Slider; @@ -141,6 +143,20 @@ where Checkbox::new(is_checked, label, f) } +pub fn radio<'a, Message, Renderer, V>( + label: impl Into, + value: V, + selected: Option, + on_click: impl FnOnce(V) -> Message, +) -> Radio<'a, Message, Renderer> +where + Message: Clone, + Renderer: iced_native::text::Renderer, + V: Copy + Eq, +{ + Radio::new(value, label, selected, on_click) +} + pub fn toggler<'a, Message, Renderer>( label: impl Into>, is_checked: bool, diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs new file mode 100644 index 0000000000..25fe5bdd99 --- /dev/null +++ b/pure/src/widget/radio.rs @@ -0,0 +1,125 @@ +use crate::{Element, Tree, 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, Hasher, Length, Point, Rectangle, Shell}; + +use std::any::{self, Any}; + +pub use iced_native::widget::Radio; + +impl<'a, Message, Renderer> Widget + for Radio<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn diff(&self, _tree: &mut Tree) {} + + fn children_state(&self) -> Vec { + Vec::new() + } + + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::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 { + >::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, + ) { + >::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 { + >::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + >::hash_layout( + self, state, + ) + } +} + +impl<'a, Message, Renderer> Into> + for Radio<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From 6689ede6d8ce0d65ec3ce29fd863ec7f26052621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 22:18:21 +0700 Subject: [PATCH 26/55] Implement `Space` in `iced_pure` --- pure/src/widget.rs | 10 ++++ pure/src/widget/space.rs | 123 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 pure/src/widget/space.rs diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 7b5fc0bc1d..9112dd9a53 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -9,6 +9,7 @@ mod radio; mod row; mod scrollable; mod slider; +mod space; mod text; mod text_input; mod toggler; @@ -24,6 +25,7 @@ pub use radio::Radio; pub use row::Row; pub use scrollable::Scrollable; pub use slider::Slider; +pub use space::Space; pub use text::Text; pub use text_input::TextInput; pub use toggler::Toggler; @@ -195,3 +197,11 @@ where pub fn image(handle: impl Into) -> Image { Image::new(handle.into()) } + +pub fn horizontal_space(width: Length) -> Space { + Space::with_width(width) +} + +pub fn vertical_space(height: Length) -> Space { + Space::with_height(height) +} diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs new file mode 100644 index 0000000000..67d17c168e --- /dev/null +++ b/pure/src/widget/space.rs @@ -0,0 +1,123 @@ +use crate::{Element, Tree, 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, Hasher, Length, Point, Rectangle, Shell}; + +use std::any::{self, Any}; + +pub use iced_native::widget::Space; + +impl<'a, Message, Renderer> Widget for Space +where + Message: Clone, + Renderer: text::Renderer, +{ + fn tag(&self) -> any::TypeId { + any::TypeId::of::<()>() + } + + fn state(&self) -> Box { + Box::new(()) + } + + fn diff(&self, _tree: &mut Tree) {} + + fn children_state(&self) -> Vec { + Vec::new() + } + + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::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 { + >::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, + ) { + >::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 { + >::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + >::hash_layout( + self, state, + ) + } +} + +impl<'a, Message, Renderer> Into> for Space +where + Message: 'a + Clone, + Renderer: text::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From cff891833be68c0e2d4919d4475daf23da821f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 13 Feb 2022 22:19:43 +0700 Subject: [PATCH 27/55] Implement `pure` version of the `tour` example :tada: --- Cargo.toml | 1 + examples/pure/tour/Cargo.toml | 10 + examples/pure/tour/README.md | 28 ++ examples/pure/tour/index.html | 12 + examples/pure/tour/src/main.rs | 702 +++++++++++++++++++++++++++++++++ 5 files changed, 753 insertions(+) create mode 100644 examples/pure/tour/Cargo.toml create mode 100644 examples/pure/tour/README.md create mode 100644 examples/pure/tour/index.html create mode 100644 examples/pure/tour/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 11966dd094..9adb2ae512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ members = [ "examples/url_handler", "examples/pure/counter", "examples/pure/todos", + "examples/pure/tour", "examples/websocket", ] diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml new file mode 100644 index 0000000000..8ce5f198ad --- /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 "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["image", "debug", "pure"] } +env_logger = "0.8" diff --git a/examples/pure/tour/README.md b/examples/pure/tour/README.md new file mode 100644 index 0000000000..e7cd2d5c63 --- /dev/null +++ b/examples/pure/tour/README.md @@ -0,0 +1,28 @@ +## Tour + +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. + +The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. + + + +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: https://github.com/iced-rs/iced_web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! + +[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage diff --git a/examples/pure/tour/index.html b/examples/pure/tour/index.html new file mode 100644 index 0000000000..c64af912c5 --- /dev/null +++ b/examples/pure/tour/index.html @@ -0,0 +1,12 @@ + + + + + + Tour - Iced + + + + + + diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs new file mode 100644 index 0000000000..7a50bcdcd8 --- /dev/null +++ b/examples/pure/tour/src/main.rs @@ -0,0 +1,702 @@ +use iced::alignment; +use iced::pure::widget::{ + checkbox, column, container, horizontal_space, image, radio, row, + scrollable, slider, text, text_input, toggler, vertical_space, +}; +use iced::pure::{Button, Column, Container, Element, Sandbox, Slider}; +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 { + 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, + 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 { + 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 }, + 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 { + 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) -> 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 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() + } + } + } +} From 35e9b75e415ef3b9124051696b60628ef56afe47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Feb 2022 15:44:50 +0700 Subject: [PATCH 28/55] Introduce `Tag` and `State` opaque types in `iced_pure::widget::tree` --- pure/src/widget.rs | 20 ++++++++------ pure/src/widget/button.rs | 21 +++++++------- pure/src/widget/checkbox.rs | 16 ----------- pure/src/widget/column.rs | 13 ++------- pure/src/widget/container.rs | 13 ++------- pure/src/widget/element.rs | 17 ++++++------ pure/src/widget/image.rs | 15 ---------- pure/src/widget/radio.rs | 16 ----------- pure/src/widget/row.rs | 14 ++-------- pure/src/widget/scrollable.rs | 20 ++++++-------- pure/src/widget/slider.rs | 16 ++++------- pure/src/widget/space.rs | 16 ----------- pure/src/widget/text.rs | 16 ----------- pure/src/widget/text_input.rs | 19 ++++--------- pure/src/widget/toggler.rs | 14 ---------- pure/src/widget/tree.rs | 52 +++++++++++++++++++++++++++++------ 16 files changed, 101 insertions(+), 197 deletions(-) diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 9112dd9a53..03b668d398 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -37,15 +37,7 @@ use iced_native::mouse; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub trait Widget { - fn tag(&self) -> any::TypeId; - - fn state(&self) -> Box; - - fn children_state(&self) -> Vec; - fn width(&self) -> Length; fn height(&self) -> Length; @@ -68,6 +60,18 @@ pub trait Widget { viewport: &Rectangle, ); + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + fn state(&self) -> tree::State { + tree::State::None + } + + fn children(&self) -> Vec { + Vec::new() + } + fn diff(&self, _tree: &mut Tree) {} fn mouse_interaction( diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 6dc1016cee..55cbf8b4ce 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -1,4 +1,5 @@ -use crate::widget::{Element, Tree, Widget}; +use crate::widget::tree::{self, Tree}; +use crate::widget::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout; @@ -10,8 +11,6 @@ use iced_native::{ }; use iced_style::button::StyleSheet; -use std::any::Any; - pub use button::State; pub struct Button<'a, Message, Renderer> { @@ -77,20 +76,20 @@ where Message: 'static + Clone, Renderer: 'static + iced_native::Renderer, { - fn tag(&self) -> std::any::TypeId { - std::any::TypeId::of::() + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn state(&self) -> Box { - Box::new(State::new()) + fn state(&self) -> tree::State { + tree::State::new(State::new()) } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] } - fn children_state(&self) -> Vec { - vec![Tree::new(&self.content)] + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) } fn width(&self) -> Length { diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs index 5352fad3f7..8aa4e845fb 100644 --- a/pure/src/widget/checkbox.rs +++ b/pure/src/widget/checkbox.rs @@ -7,8 +7,6 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub use iced_native::widget::Checkbox; impl<'a, Message, Renderer> Widget @@ -16,20 +14,6 @@ impl<'a, Message, Renderer> Widget where Renderer: text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn diff(&self, _tree: &mut Tree) {} - - fn children_state(&self) -> Vec { - Vec::new() - } - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index a9d7246e45..4ab3e00d73 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -9,7 +9,6 @@ use iced_native::{ Alignment, Clipboard, Hasher, Length, Padding, Point, Rectangle, Shell, }; -use std::any::{self, Any}; use std::u32; pub struct Column<'a, Message, Renderer> { @@ -86,22 +85,14 @@ impl<'a, Message, Renderer> Widget where Renderer: iced_native::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() } fn diff(&self, tree: &mut Tree) { tree.diff_children(&self.children); } - fn children_state(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - fn width(&self) -> Length { self.width } diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index 85ea80391d..f42b127d7e 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -11,7 +11,6 @@ use iced_native::{ Clipboard, Hasher, Layout, Length, Padding, Point, Rectangle, Shell, }; -use std::any::{self, Any}; use std::hash::Hash; use std::u32; @@ -124,22 +123,14 @@ impl<'a, Message, Renderer> Widget where Renderer: iced_native::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] } fn diff(&self, tree: &mut Tree) { tree.diff_children(std::slice::from_ref(&self.content)) } - fn children_state(&self) -> Vec { - vec![Tree::new(&self.content)] - } - fn width(&self) -> Length { self.width } diff --git a/pure/src/widget/element.rs b/pure/src/widget/element.rs index 2a137d405e..d905b924f6 100644 --- a/pure/src/widget/element.rs +++ b/pure/src/widget/element.rs @@ -1,4 +1,5 @@ -use crate::widget::{Tree, Widget}; +use crate::widget::tree::{self, Tree}; +use crate::widget::Widget; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; @@ -6,8 +7,6 @@ use iced_native::mouse; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub struct Element<'a, Message, Renderer> { widget: Box + 'a>, } @@ -66,20 +65,20 @@ where A: 'a, B: 'a, { - fn tag(&self) -> any::TypeId { + fn tag(&self) -> tree::Tag { self.widget.tag() } - fn state(&self) -> Box { + fn state(&self) -> tree::State { self.widget.state() } - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree) + fn children(&self) -> Vec { + self.widget.children() } - fn children_state(&self) -> Vec { - self.widget.children_state() + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) } fn width(&self) -> Length { diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs index 51a24ed170..ce80781336 100644 --- a/pure/src/widget/image.rs +++ b/pure/src/widget/image.rs @@ -6,7 +6,6 @@ use iced_native::renderer; use iced_native::widget::image; use iced_native::{Hasher, Length, Point, Rectangle}; -use std::any::{self, Any}; use std::hash::Hash; pub use image::Image; @@ -16,20 +15,6 @@ where Handle: Clone + Hash, Renderer: iced_native::image::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn children_state(&self) -> Vec { - Vec::new() - } - - fn diff(&self, _tree: &mut Tree) {} - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs index 25fe5bdd99..233297b327 100644 --- a/pure/src/widget/radio.rs +++ b/pure/src/widget/radio.rs @@ -7,8 +7,6 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub use iced_native::widget::Radio; impl<'a, Message, Renderer> Widget @@ -17,20 +15,6 @@ where Message: Clone, Renderer: text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn diff(&self, _tree: &mut Tree) {} - - fn children_state(&self) -> Vec { - Vec::new() - } - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index ec7e144ce1..1f2814466f 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -9,8 +9,6 @@ use iced_native::{ Alignment, Clipboard, Hasher, Length, Padding, Point, Rectangle, Shell, }; -use std::any::{self, Any}; - pub struct Row<'a, Message, Renderer> { spacing: u16, padding: Padding, @@ -77,22 +75,14 @@ impl<'a, Message, Renderer> Widget where Renderer: iced_native::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() } fn diff(&self, tree: &mut Tree) { tree.diff_children(&self.children) } - fn children_state(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - fn width(&self) -> Length { self.width } diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index badc9fc249..c3289f9e61 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -1,4 +1,4 @@ -use crate::widget::Tree; +use crate::widget::tree::{self, Tree}; use crate::{Element, Widget}; use iced_native::event::{self, Event}; @@ -10,8 +10,6 @@ use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; pub use iced_style::scrollable::StyleSheet; -use std::any::{self, Any}; - /// A widget that can vertically display an infinite amount of content with a /// scrollbar. #[allow(missing_debug_implementations)] @@ -92,20 +90,20 @@ impl<'a, Message, Renderer> Widget where Renderer: iced_native::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::() + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn state(&self) -> Box { - Box::new(scrollable::State::new()) + fn state(&self) -> tree::State { + tree::State::new(scrollable::State::new()) } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] } - fn children_state(&self) -> Vec { - vec![Tree::new(&self.content)] + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) } fn width(&self) -> Length { diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs index f659c2ed0b..691d3f18d8 100644 --- a/pure/src/widget/slider.rs +++ b/pure/src/widget/slider.rs @@ -1,7 +1,8 @@ //! Display an interactive selector of a single value from a range of values. //! //! A [`Slider`] has some local [`State`]. -use crate::{Element, Tree, Widget}; +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout; @@ -12,7 +13,6 @@ use iced_native::{ Clipboard, Hasher, Layout, Length, Point, Rectangle, Shell, Size, }; -use std::any::{self, Any}; use std::ops::RangeInclusive; pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; @@ -143,16 +143,12 @@ where Message: Clone, Renderer: iced_native::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::() + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn state(&self) -> Box { - Box::new(slider::State::new()) - } - - fn children_state(&self) -> Vec { - Vec::new() + fn state(&self) -> tree::State { + tree::State::new(slider::State::new()) } fn width(&self) -> Length { diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs index 67d17c168e..d739439800 100644 --- a/pure/src/widget/space.rs +++ b/pure/src/widget/space.rs @@ -7,8 +7,6 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub use iced_native::widget::Space; impl<'a, Message, Renderer> Widget for Space @@ -16,20 +14,6 @@ where Message: Clone, Renderer: text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn diff(&self, _tree: &mut Tree) {} - - fn children_state(&self) -> Vec { - Vec::new() - } - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index 8f157ea0fd..696d0ae1f4 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -5,28 +5,12 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Hasher, Length, Point, Rectangle}; -use std::any::{self, Any}; - pub use iced_native::widget::Text; impl Widget for Text where Renderer: text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn diff(&self, _tree: &mut Tree) {} - - fn children_state(&self) -> Vec { - Vec::new() - } - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs index e18a2bf05c..40ce140c90 100644 --- a/pure/src/widget/text_input.rs +++ b/pure/src/widget/text_input.rs @@ -1,4 +1,5 @@ -use crate::widget::{Element, Tree, Widget}; +use crate::widget::tree::{self, Tree}; +use crate::widget::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; @@ -12,8 +13,6 @@ use iced_native::{ pub use iced_style::text_input::StyleSheet; -use std::any::{self, Any}; - /// A field that can be filled with text. /// /// # Example @@ -138,18 +137,12 @@ where Message: Clone, Renderer: iced_native::text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::() - } - - fn state(&self) -> Box { - Box::new(text_input::State::new()) + fn tag(&self) -> tree::Tag { + tree::Tag::of::() } - fn diff(&self, _tree: &mut Tree) {} - - fn children_state(&self) -> Vec { - Vec::new() + fn state(&self) -> tree::State { + tree::State::new(text_input::State::new()) } fn width(&self) -> Length { diff --git a/pure/src/widget/toggler.rs b/pure/src/widget/toggler.rs index ec86fff0f0..0861986618 100644 --- a/pure/src/widget/toggler.rs +++ b/pure/src/widget/toggler.rs @@ -8,8 +8,6 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; -use std::any::{self, Any}; - pub use iced_native::widget::toggler::{Style, StyleSheet, Toggler}; impl<'a, Message, Renderer> Widget @@ -17,18 +15,6 @@ impl<'a, Message, Renderer> Widget where Renderer: text::Renderer, { - fn tag(&self) -> any::TypeId { - any::TypeId::of::<()>() - } - - fn state(&self) -> Box { - Box::new(()) - } - - fn children_state(&self) -> Vec { - Vec::new() - } - fn width(&self) -> Length { >::width(self) } diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 3a5f443316..33f5693a01 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -3,7 +3,7 @@ use crate::widget::Element; use std::any::{self, Any}; pub struct Tree { - pub tag: any::TypeId, + pub tag: Tag, pub state: State, pub children: Vec, } @@ -11,8 +11,8 @@ pub struct Tree { impl Tree { pub fn empty() -> Self { Self { - tag: any::TypeId::of::<()>(), - state: State(Box::new(())), + tag: Tag::stateless(), + state: State::None, children: Vec::new(), } } @@ -22,8 +22,8 @@ impl Tree { ) -> Self { Self { tag: element.as_widget().tag(), - state: State(element.as_widget().state()), - children: element.as_widget().children_state(), + state: element.as_widget().state(), + children: element.as_widget().children(), } } @@ -60,20 +60,56 @@ impl Tree { } } -pub struct State(Box); +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + pub fn of() -> Self + where + T: 'static, + { + Self(any::TypeId::of::()) + } + + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +pub enum State { + None, + Some(Box), +} impl State { + pub fn new(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + pub fn downcast_ref(&self) -> &T where T: 'static, { - self.0.downcast_ref().expect("Downcast widget state") + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } } pub fn downcast_mut(&mut self) -> &mut T where T: 'static, { - self.0.downcast_mut().expect("Downcast widget state") + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } } } From 019af8ddbf96680ffcee2b3407819e90575760cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Feb 2022 17:07:25 +0700 Subject: [PATCH 29/55] Add `overlay` support in `iced_pure` and port `PickList` :tada: --- native/src/widget/pick_list.rs | 604 ++++++++++++++++++++------------- pure/src/lib.rs | 13 + pure/src/overlay.rs | 21 ++ pure/src/widget.rs | 12 + pure/src/widget/button.rs | 14 + pure/src/widget/column.rs | 10 + pure/src/widget/container.rs | 14 + pure/src/widget/pick_list.rs | 245 +++++++++++++ pure/src/widget/row.rs | 10 + pure/src/widget/scrollable.rs | 14 + 10 files changed, 717 insertions(+), 240 deletions(-) create mode 100644 pure/src/overlay.rs create mode 100644 pure/src/widget/pick_list.rs diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index a200fb1330..978b0cbc3f 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>, { - menu: &'a mut menu::State, - keyboard_modifiers: &'a mut keyboard::Modifiers, - is_open: &'a mut bool, - hovered_option: &'a mut Option, - last_selection: &'a mut Option, + state: &'a mut State, on_selected: Box Message>, options: Cow<'a, [T]>, placeholder: Option, @@ -49,8 +45,9 @@ pub struct State { last_selection: Option, } -impl Default for State { - fn default() -> Self { +impl State { + /// 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 Default for State { } } +impl Default for State { + 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, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { - let State { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, - } = state; - Self { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, + state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -145,146 +136,152 @@ where } } -impl<'a, T: 'a, Message, Renderer> Widget - for PickList<'a, T, Message, Renderer> +/// Computes the layout of a [`PickList`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option, + font: &Renderer::Font, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node where - T: Clone + ToString + Eq, - [T]: ToOwned>, - Message: 'static, - Renderer: text::Renderer + 'a, + Renderer: text::Renderer, + T: ToString, { - fn width(&self) -> Length { - self.width - } + use std::f32; - fn height(&self) -> Length { - Length::Shrink - } + let limits = limits.width(width).height(Length::Shrink).pad(padding); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits - .width(self.width) - .height(Length::Shrink) - .pad(self.padding); - - let text_size = self.text_size.unwrap_or(renderer.default_size()); - let font = self.font.clone(); - - let max_width = match self.width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let (width, _) = renderer.measure( - label, - text_size, - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }; + let text_size = text_size.unwrap_or(renderer.default_size()); - let labels = self.options.iter().map(ToString::to_string); + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font.clone(), + Size::new(f32::INFINITY, f32::INFINITY), + ); - let labels_width = - labels.map(|label| measure(&label)).max().unwrap_or(100); + width.round() as u32 + }; - let placeholder_width = self - .placeholder - .as_ref() - .map(String::as_str) - .map(measure) - .unwrap_or(100); + let labels = options.iter().map(ToString::to_string); - labels_width.max(placeholder_width) - } - _ => 0, - }; + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); - let size = { - let intrinsic = Size::new( - max_width as f32 - + f32::from(text_size) - + f32::from(self.padding.left), - f32::from(text_size), - ); + let placeholder_width = placeholder.map(measure).unwrap_or(100); - limits.resolve(intrinsic).pad(self.padding) - }; + labels_width.max(placeholder_width) + } + _ => 0, + }; - layout::Node::new(size) - } + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size) + f32::from(padding.left), + f32::from(text_size), + ); - fn hash_layout(&self, state: &mut Hasher) { - use std::hash::Hash as _; + limits.resolve(intrinsic).pad(padding) + }; - match self.width { - Length::Shrink => { - self.placeholder.hash(state); + layout::Node::new(size) +} - self.options - .iter() - .map(ToString::to_string) - .for_each(|label| label.hash(state)); - } - _ => { - self.width.hash(state); - } +/// Hashes the layout attributes of a [`PickList`]. +pub fn hash_layout( + state: &mut Hasher, + width: Length, + padding: Padding, + text_size: Option, + placeholder: Option<&str>, + options: &[T], +) where + T: ToString, +{ + use std::hash::Hash as _; + + struct Marker; + std::any::TypeId::of::().hash(state); + + padding.hash(state); + text_size.hash(state); + + match width { + Length::Shrink => { + placeholder.hash(state); + + options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + width.hash(state); } } +} - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let event_status = if *self.is_open { - // TODO: Encode cursor availability in the type system - *self.is_open = - cursor_position.x < 0.0 || cursor_position.y < 0.0; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - let selected = self.selected.as_ref(); - - *self.is_open = true; - *self.hovered_option = self - .options - .iter() - .position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); - if let Some(last_selection) = self.last_selection.take() { - shell.publish((self.on_selected)(last_selection)); + let event_status = if state.is_open { + // TODO: Encode cursor availability in the type system + state.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; - *self.is_open = false; + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); - event::Status::Captured - } else { - event_status - } + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) if self.keyboard_modifiers.command() + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() && layout.bounds().contains(cursor_position) - && !*self.is_open => + && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, @@ -296,34 +293,230 @@ where } let next_option = if y < 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter()) + if let Some(selected) = selected { + find_next(selected, options.iter()) } else { - self.options.first() + options.first() } } else if y > 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter().rev()) + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) } else { - self.options.last() + options.last() } } else { None }; if let Some(next_option) = next_option { - shell.publish((self.on_selected)(next_option.clone())); + shell.publish((on_selected)(next_option.clone())); } event::Status::Captured - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - *self.keyboard_modifiers = modifiers; - + } else { event::Status::Ignored } - _ => event::Status::Ignored, } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State, + padding: Padding, + text_size: Option, + font: Renderer::Font, + options: &'a [T], + style_sheet: &dyn StyleSheet, +) -> Option> +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( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + padding: Padding, + text_size: Option, + 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 + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + hash_layout( + state, + self.width, + self.padding, + self.text_size, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || &mut self.state, + ) } fn mouse_interaction( @@ -333,14 +526,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( @@ -351,66 +537,17 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = self.selected.is_some(); - - let style = if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius, - }, - style.background, - ); - - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - - f32::from(self.padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - - if let Some(label) = self - .selected - .as_ref() - .map(ToString::to_string) - .as_ref() - .or_else(|| self.placeholder.as_ref()) - { - renderer.fill_text(Text { - content: label, - size: f32::from( - self.text_size.unwrap_or(renderer.default_size()), - ), - font: self.font.clone(), - color: is_selected - .then(|| style.text_color) - .unwrap_or(style.placeholder_color), - bounds: Rectangle { - x: bounds.x + f32::from(self.padding.left), - y: bounds.center_y(), - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }) - } + draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) } fn overlay( @@ -418,28 +555,15 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { - if *self.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut self.menu, - &self.options, - &mut self.hovered_option, - &mut self.last_selection, - ) - .width(bounds.width.round() as u16) - .padding(self.padding) - .font(self.font.clone()) - .style(self.style_sheet.menu()); - - if let Some(text_size) = self.text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } + overlay( + layout, + &mut self.state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) } } diff --git a/pure/src/lib.rs b/pure/src/lib.rs index 07f068cce1..bab3bbc7b9 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -1,3 +1,4 @@ +pub mod overlay; pub mod widget; pub(crate) mod flex; @@ -129,6 +130,18 @@ where renderer, ) } + + fn overlay( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.element.as_widget_mut().overlay( + &mut self.state.state_tree, + layout, + renderer, + ) + } } impl<'a, Message, Renderer> Into> diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs new file mode 100644 index 0000000000..b009fde861 --- /dev/null +++ b/pure/src/overlay.rs @@ -0,0 +1,21 @@ +use crate::Tree; + +use iced_native::Layout; + +pub use iced_native::overlay::*; + +pub fn from_children<'a, Message, Renderer>( + children: &'a mut [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option> { + children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget_mut().overlay(state, layout, renderer) + }) + .next() +} diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 03b668d398..6dda653d7b 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -5,6 +5,7 @@ mod checkbox; mod column; mod container; mod element; +mod pick_list; mod radio; mod row; mod scrollable; @@ -21,6 +22,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use image::Image; +pub use pick_list::PickList; pub use radio::Radio; pub use row::Row; pub use scrollable::Scrollable; @@ -34,6 +36,7 @@ pub use tree::Tree; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; use iced_native::mouse; +use iced_native::overlay; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; @@ -97,6 +100,15 @@ pub trait Widget { ) -> event::Status { event::Status::Ignored } + + fn overlay<'a>( + &'a mut self, + _state: &'a mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + None + } } pub fn container<'a, Message, Renderer>( diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 55cbf8b4ce..f5e78933f0 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -1,3 +1,4 @@ +use crate::overlay; use crate::widget::tree::{self, Tree}; use crate::widget::{Element, Widget}; @@ -206,6 +207,19 @@ where self.on_press.is_some(), ) } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> Into> diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 4ab3e00d73..1f0253356a 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -1,4 +1,5 @@ use crate::flex; +use crate::overlay; use crate::widget::{Element, Tree, Widget}; use iced_native::event::{self, Event}; @@ -216,6 +217,15 @@ where child.as_widget().hash_layout(state); } } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } } impl<'a, Message, Renderer> Into> diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index f42b127d7e..8ad6a064b9 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -5,6 +5,7 @@ use iced_native::alignment; use iced_native::event::{self, Event}; use iced_native::layout; use iced_native::mouse; +use iced_native::overlay; use iced_native::renderer; use iced_native::widget::container; use iced_native::{ @@ -237,6 +238,19 @@ where self.content.as_widget().hash_layout(state); } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> From> diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs new file mode 100644 index 0000000000..324950e188 --- /dev/null +++ b/pure/src/widget/pick_list.rs @@ -0,0 +1,245 @@ +//! Display a dropdown list of selectable values. +use crate::widget::tree::{self, Tree}; +use crate::{Element, Widget}; + +use iced_native::event::{self, Event}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::overlay; +use iced_native::renderer; +use iced_native::text; +use iced_native::widget::pick_list; +use iced_native::{ + Clipboard, Hasher, Layout, Length, Padding, Point, Rectangle, Shell, +}; + +use std::borrow::Cow; + +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: text::Renderer> +where + [T]: ToOwned>, +{ + on_selected: Box Message + 'a>, + options: Cow<'a, [T]>, + placeholder: Option, + selected: Option, + width: Length, + padding: Padding, + text_size: Option, + font: Renderer::Font, + style_sheet: Box, +} + +impl<'a, T: 'a, Message, Renderer: text::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned>, +{ + /// 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>, + selected: Option, + 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) -> 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>(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>, + ) -> Self { + self.style_sheet = style_sheet.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(pick_list::State::::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + pick_list::layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + pick_list::hash_layout( + state, + self.width, + self.padding, + self.text_size, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + pick_list::update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::>(), + ) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + pick_list::mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + pick_list::draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + let state = tree.state.downcast_mut::>(); + + 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> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned>, + Renderer: text::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 1f2814466f..2912858938 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -1,4 +1,5 @@ use crate::flex; +use crate::overlay; use crate::widget::{Element, Tree, Widget}; use iced_native::event::{self, Event}; @@ -202,6 +203,15 @@ where child.as_widget().hash_layout(state); } } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } } impl<'a, Message, Renderer> Into> diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index c3289f9e61..6653125e70 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -1,3 +1,4 @@ +use crate::overlay; use crate::widget::tree::{self, Tree}; use crate::{Element, Widget}; @@ -230,6 +231,19 @@ where }, ) } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } } impl<'a, Message, Renderer> From> From 6e242fe0e506f8086371a24f03e7fbe3a10ca2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Feb 2022 17:15:43 +0700 Subject: [PATCH 30/55] Add `pick_list` function helper in `iced_pure::widget` --- pure/src/widget.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 6dda653d7b..62f9d95b65 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -40,6 +40,8 @@ use iced_native::overlay; use iced_native::renderer; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; +use std::borrow::Cow; + pub trait Widget { fn width(&self) -> Length; @@ -210,6 +212,19 @@ where Slider::new(range, value, on_change) } +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into>, + selected: Option, + on_selected: impl Fn(T) -> Message + 'a, +) -> PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned>, + Renderer: iced_native::text::Renderer, +{ + PickList::new(options, selected, on_selected) +} + pub fn image(handle: impl Into) -> Image { Image::new(handle.into()) } From 0ca066277a296469fff95bef48e8c23e1d2b375e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Feb 2022 17:15:56 +0700 Subject: [PATCH 31/55] Fix `overlay` translation for `Scrollable` in `iced_pure` --- pure/src/widget/scrollable.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 6653125e70..8a206a6c40 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -7,7 +7,7 @@ use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::renderer; use iced_native::widget::scrollable; -use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell}; +use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Vector}; pub use iced_style::scrollable::StyleSheet; @@ -238,11 +238,24 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) + self.content + .as_widget_mut() + .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::() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) } } From 2737b21d346c9034502c345dea735ca136b49cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Feb 2022 17:16:34 +0700 Subject: [PATCH 32/55] Implement `pure` version of `pick_list` example :tada: --- Cargo.toml | 1 + examples/pure/pick_list/Cargo.toml | 9 +++ examples/pure/pick_list/README.md | 18 +++++ examples/pure/pick_list/src/main.rs | 111 ++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 examples/pure/pick_list/Cargo.toml create mode 100644 examples/pure/pick_list/README.md create mode 100644 examples/pure/pick_list/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 9adb2ae512..e94a1652d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,7 @@ members = [ "examples/tour", "examples/url_handler", "examples/pure/counter", + "examples/pure/pick_list", "examples/pure/todos", "examples/pure/tour", "examples/websocket", diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml new file mode 100644 index 0000000000..c0fcac3c4c --- /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 "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } diff --git a/examples/pure/pick_list/README.md b/examples/pure/pick_list/README.md new file mode 100644 index 0000000000..6dc80bf46d --- /dev/null +++ b/examples/pure/pick_list/README.md @@ -0,0 +1,18 @@ +## Pick-list + +A dropdown list of selectable options. + +It displays and positions an overlay based on the window position of the widget. + +The __[`main`]__ file contains all the code of the example. + +
+ +
+ +You can run it with `cargo run`: +``` +cargo run --package pick_list +``` + +[`main`]: src/main.rs diff --git a/examples/pure/pick_list/src/main.rs b/examples/pure/pick_list/src/main.rs new file mode 100644 index 0000000000..f9d55dd092 --- /dev/null +++ b/examples/pure/pick_list/src/main.rs @@ -0,0 +1,111 @@ +use iced::pure::widget::{ + 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, +} + +#[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 { + 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", + } + ) + } +} From da45b6c1627935bff5334d213096c4e78972af46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 17 Feb 2022 19:08:54 +0700 Subject: [PATCH 33/55] Implement `pure::Component` in `iced_lazy` --- lazy/Cargo.toml | 8 + lazy/src/lib.rs | 3 + lazy/src/pure.rs | 1 + lazy/src/pure/component.rs | 500 ++++++++++++++++++++++++++++++++++ pure/src/overlay.rs | 6 +- pure/src/widget.rs | 4 +- pure/src/widget/button.rs | 4 +- pure/src/widget/column.rs | 4 +- pure/src/widget/container.rs | 4 +- pure/src/widget/pick_list.rs | 2 +- pure/src/widget/row.rs | 4 +- pure/src/widget/scrollable.rs | 4 +- pure/src/widget/space.rs | 7 +- 13 files changed, 530 insertions(+), 21 deletions(-) create mode 100644 lazy/src/pure.rs create mode 100644 lazy/src/pure/component.rs diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index b840de503b..2d7451f3e3 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 05fce76570..5d7d10e458 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 0000000000..9cea807e41 --- /dev/null +++ b/lazy/src/pure.rs @@ -0,0 +1 @@ +pub mod component; diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs new file mode 100644 index 0000000000..3061e45d59 --- /dev/null +++ b/lazy/src/pure/component.rs @@ -0,0 +1,500 @@ +//! 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, Hasher, 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::hash::Hash; +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 { + /// 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; + + /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) + /// on user interaction. + fn view(&self, state: &Self::State) -> Element; +} + +/// 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 + '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>>, +} + +#[self_referencing] +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box + 'a>, + message: PhantomData, + state: PhantomData, + + #[borrows(component)] + #[covariant] + element: Option>, +} + +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where + S: Default, +{ + fn with_element( + &self, + f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, + ) -> T { + self.with_element_mut(|element| f(element)) + } + + fn with_element_mut( + &self, + f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, + ) -> T { + if self + .state + .borrow() + .as_ref() + .unwrap() + .borrow_element() + .is_none() + { + 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: |state| Some(state.view(&S::default())), + } + .build(), + ); + } + + self.state + .borrow_mut() + .as_mut() + .unwrap() + .with_element_mut(|element| f(element.as_mut().unwrap())) + } +} + +impl<'a, Message, Renderer, Event, S> Widget + for Instance<'a, Message, Renderer, Event, S> +where + S: 'static + Default, + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec { + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + 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::(), 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::())) + }, + } + .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 hash_layout(&self, state: &mut Hasher) { + self.with_element(|element| { + element.as_widget().hash_layout(state); + }); + } + + 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> { + 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>>, + + #[borrows(instance_ref, mut tree)] + #[covariant] + overlay: Option>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { + overlay: Option>, +} + +impl<'a, 'b, Message, Renderer, Event, S> + OverlayInstance<'a, 'b, Message, Renderer, Event, S> +{ + fn with_overlay_maybe( + &self, + f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option { + self.overlay + .as_ref() + .unwrap() + .borrow_overlay() + .as_ref() + .map(f) + } + + fn with_overlay_mut_maybe( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, + ) -> Option { + self.overlay + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay + 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 hash_layout(&self, state: &mut Hasher, position: Point) { + struct Marker; + std::any::TypeId::of::().hash(state); + + (position.x as u32).hash(state); + (position.y as u32).hash(state); + + self.with_overlay_maybe(|overlay| { + overlay.hash_layout(state); + }); + } + + 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::(), 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::())) + }, + } + .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/pure/src/overlay.rs b/pure/src/overlay.rs index b009fde861..72415634ae 100644 --- a/pure/src/overlay.rs +++ b/pure/src/overlay.rs @@ -5,17 +5,17 @@ use iced_native::Layout; pub use iced_native::overlay::*; pub fn from_children<'a, Message, Renderer>( - children: &'a mut [crate::Element<'_, Message, Renderer>], + children: &'a [crate::Element<'_, Message, Renderer>], tree: &'a mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { children - .iter_mut() + .iter() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) + child.as_widget().overlay(state, layout, renderer) }) .next() } diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 62f9d95b65..a12d6fad7f 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod tree; mod button; mod checkbox; @@ -14,7 +15,6 @@ mod space; mod text; mod text_input; mod toggler; -mod tree; pub use button::Button; pub use checkbox::Checkbox; @@ -104,7 +104,7 @@ pub trait Widget { } fn overlay<'a>( - &'a mut self, + &'a self, _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index f5e78933f0..2ed67a9c48 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -209,12 +209,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 1f0253356a..698d7e9c53 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -219,12 +219,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index 8ad6a064b9..c8f0b3a25b 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -240,12 +240,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.content.as_widget_mut().overlay( + self.content.as_widget().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, diff --git a/pure/src/widget/pick_list.rs b/pure/src/widget/pick_list.rs index 324950e188..9dc847ee4b 100644 --- a/pure/src/widget/pick_list.rs +++ b/pure/src/widget/pick_list.rs @@ -212,7 +212,7 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index 2912858938..1c574d5101 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -205,12 +205,12 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&self.children, tree, layout, renderer) } } diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 8a206a6c40..bbda50e520 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -233,13 +233,13 @@ where } fn overlay<'b>( - &'b mut self, + &'b self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, ) -> Option> { self.content - .as_widget_mut() + .as_widget() .overlay( &mut tree.children[0], layout.children().next().unwrap(), diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs index d739439800..e0c9c285fc 100644 --- a/pure/src/widget/space.rs +++ b/pure/src/widget/space.rs @@ -4,15 +4,13 @@ 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, Hasher, Length, Point, Rectangle, Shell}; pub use iced_native::widget::Space; impl<'a, Message, Renderer> Widget for Space where - Message: Clone, - Renderer: text::Renderer, + Renderer: iced_native::Renderer, { fn width(&self) -> Length { >::width(self) @@ -98,8 +96,7 @@ where impl<'a, Message, Renderer> Into> for Space where - Message: 'a + Clone, - Renderer: text::Renderer + 'a, + Renderer: iced_native::Renderer + 'a, { fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) From 9b23ea698e98f5731a1253410e23697329083c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 17 Feb 2022 19:09:26 +0700 Subject: [PATCH 34/55] Implement `pure` version of `component` example --- Cargo.toml | 1 + examples/pure/component/Cargo.toml | 11 ++ examples/pure/component/src/main.rs | 168 ++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 examples/pure/component/Cargo.toml create mode 100644 examples/pure/component/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index e94a1652d9..aa4bbacd15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "examples/tooltip", "examples/tour", "examples/url_handler", + "examples/pure/component", "examples/pure/counter", "examples/pure/pick_list", "examples/pure/todos", diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml new file mode 100644 index 0000000000..a15f134f2b --- /dev/null +++ b/examples/pure/component/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pure_component" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../../..", features = ["debug", "pure"] } +iced_native = { path = "../../../native" } +iced_lazy = { path = "../../../lazy", features = ["pure"] } diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs new file mode 100644 index 0000000000..0de7bdd932 --- /dev/null +++ b/examples/pure/component/src/main.rs @@ -0,0 +1,168 @@ +use iced::pure::widget::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, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + NumericInputChanged(Option), +} + +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 { + container(numeric_input(self.value, Message::NumericInputChanged)) + .padding(20) + .height(Length::Fill) + .center_y() + .into() + } +} + +mod numeric_input { + use iced::pure::widget::Element; + use iced::pure::widget::{row, text, text_input}; + use iced_lazy::pure::component::{self, Component}; + use iced_native::alignment::{self, Alignment}; + use iced_native::text; + use iced_native::Length; + + pub struct NumericInput { + value: Option, + on_change: Box) -> Message>, + } + + pub fn numeric_input( + value: Option, + on_change: impl Fn(Option) -> Message + 'static, + ) -> NumericInput { + NumericInput::new(value, on_change) + } + + #[derive(Debug, Clone)] + pub enum Event { + InputChanged(String), + IncrementPressed, + DecrementPressed, + } + + impl NumericInput { + pub fn new( + value: Option, + on_change: impl Fn(Option) -> Message + 'static, + ) -> Self { + Self { + value, + on_change: Box::new(on_change), + } + } + } + + impl Component for NumericInput + where + Renderer: text::Renderer + 'static, + { + type State = (); + type Event = Event; + + fn update( + &mut self, + _state: &mut Self::State, + event: Event, + ) -> Option { + 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 { + let button = |label, on_press| { + use iced::pure::widget::button; + + 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> + for Element<'a, Message, Renderer> + where + Message: 'a, + Renderer: 'static + text::Renderer, + { + fn from(numeric_input: NumericInput) -> Self { + component::view(numeric_input) + } + } +} From 820d332736205c7485d08352841b705205130f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Feb 2022 13:08:33 +0700 Subject: [PATCH 35/55] Fix `subscription` for `iced::pure::Application` --- src/pure/application.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pure/application.rs b/src/pure/application.rs index 395eba9a92..5f400beab5 100644 --- a/src/pure/application.rs +++ b/src/pure/application.rs @@ -158,6 +158,10 @@ where A::update(&mut self.application, message) } + fn subscription(&self) -> Subscription { + A::subscription(&self.application) + } + fn view(&mut self) -> crate::Element<'_, Self::Message> { let content = A::view(&self.application); From fa3bd4280e7c34354db0a15ef1baba9fbbeb87f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 7 Mar 2022 16:33:25 +0700 Subject: [PATCH 36/55] Initialize `lazy::pure::Component` view properly ... during `children` and `diff` in the `Widget` implementation --- lazy/src/pure/component.rs | 47 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs index c79e34f87d..ad82c55e3c 100644 --- a/lazy/src/pure/component.rs +++ b/lazy/src/pure/component.rs @@ -101,27 +101,6 @@ where &self, f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, ) -> T { - if self - .state - .borrow() - .as_ref() - .unwrap() - .borrow_element() - .is_none() - { - 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: |state| Some(state.view(&S::default())), - } - .build(), - ); - } - self.state .borrow_mut() .as_mut() @@ -145,10 +124,36 @@ where } fn children(&self) -> Vec { + 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: |state| Some(state.view(&S::default())), + } + .build(), + ); + self.with_element(|element| vec![Tree::new(element)]) } fn diff(&self, tree: &mut Tree) { + 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: |state| { + Some(state.view(tree.state.downcast_ref())) + }, + } + .build(), + ); + self.with_element(|element| { tree.diff_children(std::slice::from_ref(&element)) }) From 9fd66c820d08d3ff193ef04907686774c98d2fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 7 Mar 2022 16:39:04 +0700 Subject: [PATCH 37/55] Introduce `rebuild_element` helper in `lazy::pure::Component` --- lazy/src/pure/component.rs | 42 +++++++++++++++----------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/lazy/src/pure/component.rs b/lazy/src/pure/component.rs index ad82c55e3c..4d952f6940 100644 --- a/lazy/src/pure/component.rs +++ b/lazy/src/pure/component.rs @@ -90,6 +90,20 @@ 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( &self, f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, @@ -124,36 +138,12 @@ where } fn children(&self) -> Vec { - 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: |state| Some(state.view(&S::default())), - } - .build(), - ); - + self.rebuild_element(&S::default()); self.with_element(|element| vec![Tree::new(element)]) } fn diff(&self, tree: &mut Tree) { - 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: |state| { - Some(state.view(tree.state.downcast_ref())) - }, - } - .build(), - ); - + self.rebuild_element(tree.state.downcast_ref()); self.with_element(|element| { tree.diff_children(std::slice::from_ref(&element)) }) From b50e208f31e51c2d947aeaf9fea8a9f287f26aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 7 Mar 2022 18:04:13 +0700 Subject: [PATCH 38/55] Implement `pure::Responsive` in `iced_lazy` --- examples/pure/component/src/main.rs | 4 +- lazy/src/pure.rs | 32 ++- lazy/src/pure/responsive.rs | 365 ++++++++++++++++++++++++++++ 3 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 lazy/src/pure/responsive.rs diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs index 0de7bdd932..ab10317f25 100644 --- a/examples/pure/component/src/main.rs +++ b/examples/pure/component/src/main.rs @@ -49,7 +49,7 @@ impl Sandbox for Component { mod numeric_input { use iced::pure::widget::Element; use iced::pure::widget::{row, text, text_input}; - use iced_lazy::pure::component::{self, Component}; + use iced_lazy::pure::{self, Component}; use iced_native::alignment::{self, Alignment}; use iced_native::text; use iced_native::Length; @@ -162,7 +162,7 @@ mod numeric_input { Renderer: 'static + text::Renderer, { fn from(numeric_input: NumericInput) -> Self { - component::view(numeric_input) + pure::component(numeric_input) } } } diff --git a/lazy/src/pure.rs b/lazy/src/pure.rs index 9cea807e41..dc500e5ee2 100644 --- a/lazy/src/pure.rs +++ b/lazy/src/pure.rs @@ -1 +1,31 @@ -pub mod component; +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 + '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/responsive.rs b/lazy/src/pure/responsive.rs new file mode 100644 index 0000000000..2b77873fe9 --- /dev/null +++ b/lazy/src/pure/responsive.rs @@ -0,0 +1,365 @@ +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::overlay; +use iced_pure::widget::horizontal_space; +use iced_pure::widget::tree::{self, Tree}; +use iced_pure::{Element, Widget}; + +use ouroboros::self_referencing; +use std::cell::{Ref, 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 Element<'a, Message, Renderer> + 'a>, + content: RefCell>, +} + +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( + &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, + { + 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, +} + +impl<'a, Message, Renderer> Widget + for Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + 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::(); + 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::(); + 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::(); + 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> { + let state = tree.state.downcast_ref::(); + + let overlay = OverlayBuilder { + content: self.content.borrow(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content, tree| { + content.element.as_widget().overlay(tree, 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> + 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: Ref<'a, Content<'b, Message, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData, + + #[borrows(content, mut tree)] + #[covariant] + overlay: Option>, +} + +impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { + fn with_overlay_maybe( + &self, + f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + self.borrow_overlay().as_ref().map(f) + } + + fn with_overlay_mut_maybe( + &mut self, + f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, + ) -> Option { + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + } +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay + 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) + } +} From 7d9ab71790ba0395681490f3af4d3899bb09ab09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 8 Mar 2022 22:08:38 +0700 Subject: [PATCH 39/55] Remove superfluous files from `pure` examples --- examples/pure/counter/README.md | 18 ------------------ examples/pure/counter/index.html | 12 ------------ examples/pure/pick_list/README.md | 18 ------------------ examples/pure/todos/README.md | 20 -------------------- examples/pure/todos/index.html | 12 ------------ examples/pure/tour/README.md | 28 ---------------------------- examples/pure/tour/index.html | 12 ------------ 7 files changed, 120 deletions(-) delete mode 100644 examples/pure/counter/README.md delete mode 100644 examples/pure/counter/index.html delete mode 100644 examples/pure/pick_list/README.md delete mode 100644 examples/pure/todos/README.md delete mode 100644 examples/pure/todos/index.html delete mode 100644 examples/pure/tour/README.md delete mode 100644 examples/pure/tour/index.html diff --git a/examples/pure/counter/README.md b/examples/pure/counter/README.md deleted file mode 100644 index 4d9fc5b979..0000000000 --- a/examples/pure/counter/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## Counter - -The classic counter example explained in the [`README`](../../README.md). - -The __[`main`]__ file contains all the code of the example. - - - -You can run it with `cargo run`: -``` -cargo run --package counter -``` - -[`main`]: src/main.rs diff --git a/examples/pure/counter/index.html b/examples/pure/counter/index.html deleted file mode 100644 index d2e368e4eb..0000000000 --- a/examples/pure/counter/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Counter - Iced - - - - - - diff --git a/examples/pure/pick_list/README.md b/examples/pure/pick_list/README.md deleted file mode 100644 index 6dc80bf46d..0000000000 --- a/examples/pure/pick_list/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## Pick-list - -A dropdown list of selectable options. - -It displays and positions an overlay based on the window position of the widget. - -The __[`main`]__ file contains all the code of the example. - -
- -
- -You can run it with `cargo run`: -``` -cargo run --package pick_list -``` - -[`main`]: src/main.rs diff --git a/examples/pure/todos/README.md b/examples/pure/todos/README.md deleted file mode 100644 index 9c2598b95e..0000000000 --- a/examples/pure/todos/README.md +++ /dev/null @@ -1,20 +0,0 @@ -## Todos - -A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. - -All the example code is located in the __[`main`]__ file. - - - -You can run the native version with `cargo run`: -``` -cargo run --package todos -``` -We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! - -[`main`]: src/main.rs -[TodoMVC]: http://todomvc.com/ diff --git a/examples/pure/todos/index.html b/examples/pure/todos/index.html deleted file mode 100644 index ee5570fb9e..0000000000 --- a/examples/pure/todos/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Todos - Iced - - - - - - diff --git a/examples/pure/tour/README.md b/examples/pure/tour/README.md deleted file mode 100644 index e7cd2d5c63..0000000000 --- a/examples/pure/tour/README.md +++ /dev/null @@ -1,28 +0,0 @@ -## Tour - -A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. - -The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. - - - -[`main`]: src/main.rs -[`iced_winit`]: ../../winit -[`iced_native`]: ../../native -[`iced_wgpu`]: ../../wgpu -[`iced_web`]: https://github.com/iced-rs/iced_web -[`winit`]: https://github.com/rust-windowing/winit -[`wgpu`]: https://github.com/gfx-rs/wgpu-rs - -You can run the native version with `cargo run`: -``` -cargo run --package tour -``` - -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! - -[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage diff --git a/examples/pure/tour/index.html b/examples/pure/tour/index.html deleted file mode 100644 index c64af912c5..0000000000 --- a/examples/pure/tour/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Tour - Iced - - - - - - From 12c1a3f829c801022d45f1a294d8fc7fa10606e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Mar 2022 14:10:15 +0700 Subject: [PATCH 40/55] Remove redundant `widget` modules in subcrates Instead, we can define the type aliases just once in the root crate! --- Cargo.toml | 14 +- glow/src/lib.rs | 4 - glow/src/widget.rs | 79 ---------- glow/src/widget/button.rs | 13 -- glow/src/widget/canvas.rs | 6 - glow/src/widget/checkbox.rs | 10 -- glow/src/widget/container.rs | 11 -- glow/src/widget/pane_grid.rs | 32 ---- glow/src/widget/pick_list.rs | 9 -- glow/src/widget/progress_bar.rs | 6 - glow/src/widget/qr_code.rs | 2 - glow/src/widget/radio.rs | 10 -- glow/src/widget/rule.rs | 3 - glow/src/widget/scrollable.rs | 13 -- glow/src/widget/slider.rs | 5 - glow/src/widget/text_input.rs | 13 -- glow/src/widget/toggler.rs | 10 -- glow/src/widget/tooltip.rs | 6 - graphics/src/layer.rs | 5 +- graphics/src/renderer.rs | 30 ++++ graphics/src/widget.rs | 65 +-------- graphics/src/widget/button.rs | 12 -- graphics/src/widget/checkbox.rs | 10 -- graphics/src/widget/column.rs | 5 - graphics/src/widget/container.rs | 11 -- graphics/src/widget/image.rs | 25 ---- graphics/src/widget/image/viewer.rs | 2 - graphics/src/widget/pane_grid.rs | 26 ---- graphics/src/widget/pick_list.rs | 9 -- graphics/src/widget/progress_bar.rs | 5 - graphics/src/widget/radio.rs | 11 -- graphics/src/widget/row.rs | 5 - graphics/src/widget/rule.rs | 3 - graphics/src/widget/scrollable.rs | 13 -- graphics/src/widget/slider.rs | 5 - graphics/src/widget/space.rs | 1 - graphics/src/widget/svg.rs | 20 --- graphics/src/widget/text.rs | 7 - graphics/src/widget/text_input.rs | 13 -- graphics/src/widget/toggler.rs | 10 -- graphics/src/widget/tooltip.rs | 11 -- native/src/widget/scrollable.rs | 7 + native/src/widget/toggler.rs | 3 +- src/pure.rs | 1 + src/widget.rs | 219 +++++++++++++++++++++++----- wgpu/src/lib.rs | 4 - wgpu/src/widget.rs | 79 ---------- wgpu/src/widget/button.rs | 13 -- wgpu/src/widget/canvas.rs | 6 - wgpu/src/widget/checkbox.rs | 10 -- wgpu/src/widget/container.rs | 11 -- wgpu/src/widget/pane_grid.rs | 32 ---- wgpu/src/widget/pick_list.rs | 9 -- wgpu/src/widget/progress_bar.rs | 5 - wgpu/src/widget/qr_code.rs | 2 - wgpu/src/widget/radio.rs | 10 -- wgpu/src/widget/rule.rs | 3 - wgpu/src/widget/scrollable.rs | 13 -- wgpu/src/widget/slider.rs | 5 - wgpu/src/widget/text_input.rs | 13 -- wgpu/src/widget/toggler.rs | 10 -- wgpu/src/widget/tooltip.rs | 6 - 62 files changed, 228 insertions(+), 783 deletions(-) delete mode 100644 glow/src/widget.rs delete mode 100644 glow/src/widget/button.rs delete mode 100644 glow/src/widget/canvas.rs delete mode 100644 glow/src/widget/checkbox.rs delete mode 100644 glow/src/widget/container.rs delete mode 100644 glow/src/widget/pane_grid.rs delete mode 100644 glow/src/widget/pick_list.rs delete mode 100644 glow/src/widget/progress_bar.rs delete mode 100644 glow/src/widget/qr_code.rs delete mode 100644 glow/src/widget/radio.rs delete mode 100644 glow/src/widget/rule.rs delete mode 100644 glow/src/widget/scrollable.rs delete mode 100644 glow/src/widget/slider.rs delete mode 100644 glow/src/widget/text_input.rs delete mode 100644 glow/src/widget/toggler.rs delete mode 100644 glow/src/widget/tooltip.rs delete mode 100644 graphics/src/widget/button.rs delete mode 100644 graphics/src/widget/checkbox.rs delete mode 100644 graphics/src/widget/column.rs delete mode 100644 graphics/src/widget/container.rs delete mode 100644 graphics/src/widget/image.rs delete mode 100644 graphics/src/widget/image/viewer.rs delete mode 100644 graphics/src/widget/pane_grid.rs delete mode 100644 graphics/src/widget/pick_list.rs delete mode 100644 graphics/src/widget/progress_bar.rs delete mode 100644 graphics/src/widget/radio.rs delete mode 100644 graphics/src/widget/row.rs delete mode 100644 graphics/src/widget/rule.rs delete mode 100644 graphics/src/widget/scrollable.rs delete mode 100644 graphics/src/widget/slider.rs delete mode 100644 graphics/src/widget/space.rs delete mode 100644 graphics/src/widget/svg.rs delete mode 100644 graphics/src/widget/text.rs delete mode 100644 graphics/src/widget/text_input.rs delete mode 100644 graphics/src/widget/toggler.rs delete mode 100644 graphics/src/widget/tooltip.rs delete mode 100644 wgpu/src/widget.rs delete mode 100644 wgpu/src/widget/button.rs delete mode 100644 wgpu/src/widget/canvas.rs delete mode 100644 wgpu/src/widget/checkbox.rs delete mode 100644 wgpu/src/widget/container.rs delete mode 100644 wgpu/src/widget/pane_grid.rs delete mode 100644 wgpu/src/widget/pick_list.rs delete mode 100644 wgpu/src/widget/progress_bar.rs delete mode 100644 wgpu/src/widget/qr_code.rs delete mode 100644 wgpu/src/widget/radio.rs delete mode 100644 wgpu/src/widget/rule.rs delete mode 100644 wgpu/src/widget/scrollable.rs delete mode 100644 wgpu/src/widget/slider.rs delete mode 100644 wgpu/src/widget/text_input.rs delete mode 100644 wgpu/src/widget/toggler.rs delete mode 100644 wgpu/src/widget/tooltip.rs diff --git a/Cargo.toml b/Cargo.toml index aa4bbacd15..741230468c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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) @@ -101,6 +97,8 @@ members = [ [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 } diff --git a/glow/src/lib.rs b/glow/src/lib.rs index 4e5a75d765..05435b5495 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 ee2810f96a..0000000000 --- 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; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs deleted file mode 100644 index f11ff25e0b..0000000000 --- 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 399dd19c6d..0000000000 --- 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 76d572d97d..0000000000 --- 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 c16db50d36..0000000000 --- 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 3c47acf0e2..0000000000 --- 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. -//! -//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. -/// -/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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 4d93be6840..0000000000 --- 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 413e6fb7c0..0000000000 --- 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 7b1c2408c4..0000000000 --- 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 9ef1d7a568..0000000000 --- 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 402817733b..0000000000 --- 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 d5635ec5d4..0000000000 --- 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 2fb3d5d935..0000000000 --- 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 5560e3e0d3..0000000000 --- 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 403790259c..0000000000 --- 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 c6af39038b..0000000000 --- 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/src/layer.rs b/graphics/src/layer.rs index 7a32c8509f..935062586d 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 c32eb471a4..cb31ea5fc0 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 image::Renderer for Renderer +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 svg::Renderer for Renderer +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 e34d267f0f..e7fab97c17 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; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs deleted file mode 100644 index 7b40c47b8f..0000000000 --- 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>; diff --git a/graphics/src/widget/checkbox.rs b/graphics/src/widget/checkbox.rs deleted file mode 100644 index 0d2e93f9fb..0000000000 --- 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>; diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs deleted file mode 100644 index 561681d5f3..0000000000 --- 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>; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs deleted file mode 100644 index 99996f3b12..0000000000 --- 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>; diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs deleted file mode 100644 index 761524840a..0000000000 --- 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 image::Renderer for Renderer -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 9260990a75..0000000000 --- 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 9518992096..0000000000 --- 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. -//! -//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. -/// -/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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>; diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs deleted file mode 100644 index f3ac12b8ba..0000000000 --- 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>; diff --git a/graphics/src/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs deleted file mode 100644 index 3666ecfd7d..0000000000 --- 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/radio.rs b/graphics/src/widget/radio.rs deleted file mode 100644 index 20d727476c..0000000000 --- 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>; diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs deleted file mode 100644 index 5bee3fd511..0000000000 --- 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>; diff --git a/graphics/src/widget/rule.rs b/graphics/src/widget/rule.rs deleted file mode 100644 index b96924faf5..0000000000 --- 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 3fdaf66855..0000000000 --- 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>; diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs deleted file mode 100644 index 96dc6ec402..0000000000 --- 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 77e93dbbd3..0000000000 --- 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 5817a55296..0000000000 --- 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 svg::Renderer for Renderer -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 43516fca14..0000000000 --- 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 = iced_native::widget::Text>; diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs deleted file mode 100644 index 87384d7e09..0000000000 --- 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>; diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs deleted file mode 100644 index 9053e6eda5..0000000000 --- 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>; diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs deleted file mode 100644 index 7dc12ed413..0000000000 --- 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>; - -pub use iced_native::widget::tooltip::Position; diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 3752fd71d9..748fd27d31 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)] diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index a784787177..536aef786c 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 /// diff --git a/src/pure.rs b/src/pure.rs index 5cab5fd9c5..2a2984db0c 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -41,6 +41,7 @@ pub type Button<'a, Message> = iced_pure::Button<'a, Message, crate::Renderer>; /// A pure text widget. pub type Text = iced_pure::Text; +#[cfg(feature = "image")] /// A pure image widget. pub type Image = iced_pure::Image; diff --git a/src/widget.rs b/src/widget.rs index c619bcfab8..9cc0832f01 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; + +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. + //! + //! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. + /// + /// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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; + + 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 fb03854b12..5d4f5edd54 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 99ae0ac21c..0000000000 --- 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; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs deleted file mode 100644 index f11ff25e0b..0000000000 --- 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 399dd19c6d..0000000000 --- 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 76d572d97d..0000000000 --- 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 c16db50d36..0000000000 --- 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 38bdb672d8..0000000000 --- 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. -//! -//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. -/// -/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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 4d93be6840..0000000000 --- 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 88391ccb63..0000000000 --- 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 7b1c2408c4..0000000000 --- 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 9ef1d7a568..0000000000 --- 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 402817733b..0000000000 --- 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 d5635ec5d4..0000000000 --- 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 2fb3d5d935..0000000000 --- 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 5560e3e0d3..0000000000 --- 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 7ef5e22e46..0000000000 --- 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 c6af39038b..0000000000 --- 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; From c52fd089f102be4e2ac07952106e2b6924e72e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Mar 2022 18:27:22 +0700 Subject: [PATCH 41/55] Use associated type for `Message` in a `canvas::Program` --- examples/bezier_tool/src/main.rs | 4 +++- examples/clock/src/main.rs | 4 +++- examples/color_palette/src/main.rs | 4 +++- examples/game_of_life/src/main.rs | 4 +++- examples/solar_system/src/main.rs | 4 +++- graphics/src/widget/canvas.rs | 26 ++++++++++++-------------- graphics/src/widget/canvas/program.rs | 15 ++++++++++----- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 35b5182c61..fc7ef0c918 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -104,7 +104,9 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl<'a> canvas::Program for Bezier<'a> { + type Message = Curve; + fn update( &mut self, event: Event, diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 325ccc1ad8..41d160c15e 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -76,7 +76,9 @@ impl Application for Clock { } } -impl canvas::Program for Clock { +impl canvas::Program for Clock { + type Message = Message; + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { 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 ad3004b031..c9c0637ec7 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -235,7 +235,9 @@ impl Theme { } } -impl canvas::Program for Theme { +impl canvas::Program for Theme { + type Message = Message; + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { let theme = self.canvas_cache.draw(bounds.size(), |frame| { self.draw(frame); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ab8b80e4e1..43ef8ffddb 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -328,7 +328,9 @@ mod grid { } } - impl<'a> canvas::Program for Grid { + impl<'a> canvas::Program for Grid { + type Message = Message; + fn update( &mut self, event: Event, diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 12184dd19d..bf96fee0c8 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -128,7 +128,9 @@ impl State { } } -impl canvas::Program for State { +impl canvas::Program for State { + type Message = Message; + fn draw( &self, bounds: Rectangle, diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index 65d7e37e15..ced2ce53a3 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -12,7 +12,6 @@ use iced_native::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -use std::marker::PhantomData; pub mod event; pub mod path; @@ -73,7 +72,9 @@ pub use text::Text; /// } /// /// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { +/// impl Program for Circle { +/// type Message = (); +/// /// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(bounds.size()); @@ -93,14 +94,13 @@ pub use text::Text; /// let canvas = Canvas::new(Circle { radius: 50.0 }); /// ``` #[derive(Debug)] -pub struct Canvas> { +pub struct Canvas { width: Length, height: Length, program: P, - phantom: PhantomData, } -impl> Canvas { +impl Canvas

{ const DEFAULT_SIZE: u16 = 100; /// Creates a new [`Canvas`]. @@ -109,7 +109,6 @@ impl> Canvas { width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), program, - phantom: PhantomData, } } @@ -126,9 +125,9 @@ impl> Canvas { } } -impl Widget> for Canvas +impl Widget> for Canvas

where - P: Program, + P: Program, B: Backend, { fn width(&self) -> Length { @@ -157,7 +156,7 @@ where cursor_position: Point, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, + shell: &mut Shell<'_, P::Message>, ) -> event::Status { let bounds = layout.bounds(); @@ -232,14 +231,13 @@ where } } -impl<'a, Message, P, B> From> - for Element<'a, Message, Renderer> +impl<'a, P, B> From> for Element<'a, P::Message, Renderer> where - Message: 'static, - P: Program + 'a, + P::Message: 'static, + P: Program + 'a, B: Backend, { - fn from(canvas: Canvas) -> Element<'a, Message, Renderer> { + fn from(canvas: Canvas

) -> Element<'a, P::Message, Renderer> { Element::new(canvas) } } diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index 85a2f67b30..f8b9ff2b69 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -8,7 +8,10 @@ use iced_native::{mouse, Rectangle}; /// application. /// /// [`Canvas`]: crate::widget::Canvas -pub trait Program { +pub trait Program { + /// The [`Message`] produced by the [`Program`]. + type Message; + /// Updates the state of the [`Program`]. /// /// When a [`Program`] is used in a [`Canvas`], the runtime will call this @@ -25,7 +28,7 @@ pub trait Program { _event: Event, _bounds: Rectangle, _cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { (event::Status::Ignored, None) } @@ -53,16 +56,18 @@ pub trait Program { } } -impl Program for &mut T +impl Program for &mut T where - T: Program, + T: Program, { + type Message = T::Message; + fn update( &mut self, event: Event, bounds: Rectangle, cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { T::update(self, event, bounds, cursor) } From 0cddb3c1b55017cda29d32924514e917a389f11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Mar 2022 18:59:40 +0700 Subject: [PATCH 42/55] Implement `pure` version of `Canvas` widget --- Cargo.toml | 2 +- graphics/Cargo.toml | 6 + graphics/src/widget.rs | 3 + graphics/src/widget/pure.rs | 6 + graphics/src/widget/pure/canvas.rs | 229 +++++++++++++++++++++ graphics/src/widget/pure/canvas/program.rs | 104 ++++++++++ src/pure.rs | 6 + 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 graphics/src/widget/pure.rs create mode 100644 graphics/src/widget/pure/canvas.rs create mode 100644 graphics/src/widget/pure/canvas/program.rs diff --git a/Cargo.toml b/Cargo.toml index 741230468c..f36f74f058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ 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"] +pure = ["iced_pure", "iced_graphics/pure"] [badges] maintenance = { status = "actively-developed" } diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 8ccc784936..a84acbd6e3 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/widget.rs b/graphics/src/widget.rs index e7fab97c17..cf500a69b9 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -14,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/pure.rs b/graphics/src/widget/pure.rs new file mode 100644 index 0000000000..3ecbadf1c6 --- /dev/null +++ b/graphics/src/widget/pure.rs @@ -0,0 +1,6 @@ +//! Leverage pure, virtual widgets in your application. +#[cfg(feature = "canvas")] +pub mod canvas; + +#[cfg(feature = "canvas")] +pub use canvas::Canvas; diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs new file mode 100644 index 0000000000..614553ad0b --- /dev/null +++ b/graphics/src/widget/pure/canvas.rs @@ -0,0 +1,229 @@ +//! 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::widget::{Element, Widget}; + +/// 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 Message = (); +/// type State = (); +/// +/// fn draw(&self, _state: &(), bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // 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

+where + P: Program, +{ + width: Length, + height: Length, + program: P, +} + +impl

Canvas

+where + P: Program, +{ + 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, + } + } + + /// 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 Widget> for Canvas

+where + P: Program, + B: Backend, +{ + 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, + 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, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, P::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::(); + + 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, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + _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::(); + + 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, P, B> From> for Element<'a, P::Message, Renderer> +where + P::Message: 'static, + P: Program + 'a, + B: Backend, +{ + fn from(canvas: Canvas

) -> Element<'a, P::Message, Renderer> { + 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 0000000000..cb52910dc7 --- /dev/null +++ b/graphics/src/widget/pure/canvas/program.rs @@ -0,0 +1,104 @@ +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 { + /// The [`Message`] produced by the [`Program`]. + type 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) { + (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; + + /// 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 Program for &T +where + T: Program, +{ + type Message = T::Message; + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec { + 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/src/pure.rs b/src/pure.rs index 2a2984db0c..0fc025283f 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -48,5 +48,11 @@ pub type Image = iced_pure::Image; mod application; mod sandbox; +#[cfg(feature = "canvas")] +pub use iced_graphics::widget::pure::canvas; + +#[cfg(feature = "canvas")] +pub use canvas::Canvas; + pub use application::Application; pub use sandbox::Sandbox; From 7d7064a44dcda8251bd5ef53765ba025159a0bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Mar 2022 19:09:40 +0700 Subject: [PATCH 43/55] Implement `pure` version of `game_of_life` example :tada: --- Cargo.toml | 1 + examples/pure/game_of_life/Cargo.toml | 13 + examples/pure/game_of_life/README.md | 22 + examples/pure/game_of_life/src/main.rs | 899 +++++++++++++++++++++++ examples/pure/game_of_life/src/preset.rs | 142 ++++ examples/pure/game_of_life/src/style.rs | 186 +++++ src/pure.rs | 6 +- 7 files changed, 1266 insertions(+), 3 deletions(-) create mode 100644 examples/pure/game_of_life/Cargo.toml create mode 100644 examples/pure/game_of_life/README.md create mode 100644 examples/pure/game_of_life/src/main.rs create mode 100644 examples/pure/game_of_life/src/preset.rs create mode 100644 examples/pure/game_of_life/src/style.rs diff --git a/Cargo.toml b/Cargo.toml index f36f74f058..15a4801d1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ members = [ "examples/url_handler", "examples/pure/component", "examples/pure/counter", + "examples/pure/game_of_life", "examples/pure/pick_list", "examples/pure/todos", "examples/pure/tour", diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml new file mode 100644 index 0000000000..22e38f00c8 --- /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 "] +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 0000000000..aa39201c86 --- /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. + +

+ +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 0000000000..75a0deef3c --- /dev/null +++ b/examples/pure/game_of_life/src/main.rs @@ -0,0 +1,899 @@ +//! 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::widget::{ + 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, + 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) { + ( + 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 { + 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 { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&self) -> Element { + 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::canvas::event::{self, Event}; + use iced::pure::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), + Ticked { + result: Result, + 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> { + 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 for Grid { + type Message = Message; + type State = Interaction; + + fn update( + &self, + interaction: &mut Interaction, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + 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 { + 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, + 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 { + 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>> { + 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, + } + + 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 { + self.cells.iter() + } + } + + impl std::iter::FromIterator for Life { + fn from_iter>(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 { + 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 { + 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 { + 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 { + 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, + ) -> impl Iterator { + 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 0000000000..05157b6a96 --- /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 0000000000..1a64cf4a7c --- /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/src/pure.rs b/src/pure.rs index 0fc025283f..f3f73bba2e 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -48,11 +48,11 @@ pub type Image = iced_pure::Image; mod application; mod sandbox; +pub use application::Application; +pub use sandbox::Sandbox; + #[cfg(feature = "canvas")] pub use iced_graphics::widget::pure::canvas; #[cfg(feature = "canvas")] pub use canvas::Canvas; - -pub use application::Application; -pub use sandbox::Sandbox; From 31d814b43c167ac63aa29e7a65820e3c83eb4b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 9 Mar 2022 19:19:21 +0700 Subject: [PATCH 44/55] Implement `Widget::tag` for `pure::Canvas` --- graphics/src/widget/pure/canvas.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs index 614553ad0b..912143925a 100644 --- a/graphics/src/widget/pure/canvas.rs +++ b/graphics/src/widget/pure/canvas.rs @@ -105,6 +105,10 @@ where P: Program, B: Backend, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + fn state(&self) -> tree::State { tree::State::new(P::State::default()) } From 0fbd1d98b5534a85eaa8bff40f5fa1d395edc977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 10 Mar 2022 16:58:55 +0700 Subject: [PATCH 45/55] Implement `pure` version of `Rule` widget --- native/src/widget/rule.rs | 12 ++--- pure/src/widget.rs | 12 +++++ pure/src/widget/rule.rs | 98 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 pure/src/widget/rule.rs diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index b0cc376864..69619583ef 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/pure/src/widget.rs b/pure/src/widget.rs index c516c1f247..bee216331c 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod rule; pub mod tree; mod button; @@ -25,6 +26,7 @@ pub use image::Image; pub use pick_list::PickList; pub use radio::Radio; pub use row::Row; +pub use rule::Rule; pub use scrollable::Scrollable; pub use slider::Slider; pub use space::Space; @@ -234,3 +236,13 @@ pub fn horizontal_space(width: Length) -> Space { pub fn vertical_space(height: Length) -> Space { Space::with_height(height) } + +/// Creates a horizontal [`Rule`] with the given height. +pub fn horizontal_rule<'a>(height: u16) -> Rule<'a> { + Rule::horizontal(height) +} + +/// Creates a vertical [`Rule`] with the given width. +pub fn vertical_rule<'a>(width: u16) -> Rule<'a> { + Rule::horizontal(width) +} diff --git a/pure/src/widget/rule.rs b/pure/src/widget/rule.rs new file mode 100644 index 0000000000..375bed9efa --- /dev/null +++ b/pure/src/widget/rule.rs @@ -0,0 +1,98 @@ +use crate::{Element, Tree, 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 for Rule<'a> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::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 { + >::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, + ) { + >::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 { + >::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into> for Rule<'a> +where + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From 3efb59dea3d206a9d627ce5a7a7a93c00d769ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 10 Mar 2022 17:01:57 +0700 Subject: [PATCH 46/55] Implement `pure` version of `ProgressBar` widget --- pure/src/widget.rs | 15 +++++ pure/src/widget/progress_bar.rs | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 pure/src/widget/progress_bar.rs diff --git a/pure/src/widget.rs b/pure/src/widget.rs index bee216331c..8f2cf92025 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod progress_bar; pub mod rule; pub mod tree; @@ -24,6 +25,7 @@ pub use container::Container; pub use element::Element; pub use image::Image; pub use pick_list::PickList; +pub use progress_bar::ProgressBar; pub use radio::Radio; pub use row::Row; pub use rule::Rule; @@ -43,6 +45,7 @@ use iced_native::renderer; use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; use std::borrow::Cow; +use std::ops::RangeInclusive; pub trait Widget { fn width(&self) -> Length; @@ -246,3 +249,15 @@ pub fn horizontal_rule<'a>(height: u16) -> Rule<'a> { pub fn vertical_rule<'a>(width: u16) -> Rule<'a> { 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, + value: f32, +) -> ProgressBar<'a> { + ProgressBar::new(range, value) +} diff --git a/pure/src/widget/progress_bar.rs b/pure/src/widget/progress_bar.rs new file mode 100644 index 0000000000..9b996f0250 --- /dev/null +++ b/pure/src/widget/progress_bar.rs @@ -0,0 +1,99 @@ +use crate::{Element, Tree, 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 for ProgressBar<'a> +where + Renderer: iced_native::Renderer, +{ + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::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 { + >::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, + ) { + >::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 { + >::mouse_interaction( + self, + layout, + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> Into> + for ProgressBar<'a> +where + Renderer: iced_native::Renderer + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} From 9f27969d14232355ad628431fb67aa07e42e768f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 14 Mar 2022 17:41:21 +0700 Subject: [PATCH 47/55] Fix incorrect `layout` in `Widget::overlay` for `pure::Responsive` --- lazy/src/pure/responsive.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lazy/src/pure/responsive.rs b/lazy/src/pure/responsive.rs index 2b77873fe9..291f1aaf6d 100644 --- a/lazy/src/pure/responsive.rs +++ b/lazy/src/pure/responsive.rs @@ -9,7 +9,7 @@ use iced_pure::widget::tree::{self, Tree}; use iced_pure::{Element, Widget}; use ouroboros::self_referencing; -use std::cell::{Ref, RefCell, RefMut}; +use std::cell::{RefCell, RefMut}; use std::marker::PhantomData; use std::ops::Deref; @@ -236,11 +236,27 @@ where let state = tree.state.downcast_ref::(); let overlay = OverlayBuilder { - content: self.content.borrow(), + content: self.content.borrow_mut(), tree: state.tree.borrow_mut(), types: PhantomData, overlay_builder: |content, tree| { - content.element.as_widget().overlay(tree, layout, renderer) + 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(); @@ -267,11 +283,11 @@ where #[self_referencing] struct Overlay<'a, 'b, Message, Renderer> { - content: Ref<'a, Content<'b, Message, Renderer>>, + content: RefMut<'a, Content<'b, Message, Renderer>>, tree: RefMut<'a, Tree>, types: PhantomData, - #[borrows(content, mut tree)] + #[borrows(mut content, mut tree)] #[covariant] overlay: Option>, } From 6dd187ff0822230f084e43636b1aabeb1baf06f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 10 Mar 2022 19:25:57 +0700 Subject: [PATCH 48/55] Implement `pure` version of `PaneGrid` widget --- Cargo.toml | 1 + examples/pure/pane_grid/Cargo.toml | 11 + examples/pure/pane_grid/src/main.rs | 437 +++++++++++++ native/src/widget/pane_grid.rs | 781 +++++++++++++---------- native/src/widget/pane_grid/axis.rs | 7 +- native/src/widget/pane_grid/content.rs | 39 +- native/src/widget/pane_grid/draggable.rs | 12 + native/src/widget/pane_grid/state.rs | 114 ++-- pure/src/widget.rs | 2 + pure/src/widget/pane_grid.rs | 399 ++++++++++++ pure/src/widget/pane_grid/content.rs | 331 ++++++++++ pure/src/widget/pane_grid/title_bar.rs | 355 +++++++++++ pure/src/widget/tree.rs | 17 +- 13 files changed, 2098 insertions(+), 408 deletions(-) create mode 100644 examples/pure/pane_grid/Cargo.toml create mode 100644 examples/pure/pane_grid/src/main.rs create mode 100644 native/src/widget/pane_grid/draggable.rs create mode 100644 pure/src/widget/pane_grid.rs create mode 100644 pure/src/widget/pane_grid/content.rs create mode 100644 pure/src/widget/pane_grid/title_bar.rs diff --git a/Cargo.toml b/Cargo.toml index 15a4801d1b..c6ccc5df5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ members = [ "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", diff --git a/examples/pure/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml new file mode 100644 index 0000000000..a51cdaf0bb --- /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 "] +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 0000000000..6232d6be9d --- /dev/null +++ b/examples/pure/pane_grid/src/main.rs @@ -0,0 +1,437 @@ +use iced::alignment::{self, Alignment}; +use iced::executor; +use iced::keyboard; +use iced::pure::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::Map, + panes_created: usize, + focus: Option, +} + +#[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) { + let (panes, _) = pane_grid::Map::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 { + 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 { + 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 { + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| { + let is_focused = focus == Some(id); + + let Pane { id, is_pinned } = pane; + + let pin_button = + button(text(if *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(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, *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, *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 { + 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/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 8ad63cf167..2093886e6a 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: &Renderer, + limits: &layout::Limits, + state: &state::Internal, + width: Length, + height: Length, + spacing: u16, + elements: impl Iterator, + 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, + on_click: &Option Message + 'a>>, + on_drag: &Option Message + 'a>>, + on_resize: &Option<(u16, Box 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, + on_click: &Option Message + 'a>>, + on_drag: &Option 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, +) -> Option { + 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( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + renderer: &mut Renderer, + style: &renderer::Style, + viewport: &Rectangle, + spacing: u16, + resize_leeway: Option, + style_sheet: &dyn StyleSheet, + elements: impl Iterator, + 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 2320cb7ca2..02bde06493 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 8b0e8d2a00..f0ed042667 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 for Content<'a, Message, Renderer> where T: Into>, diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs new file mode 100644 index 0000000000..6044871d54 --- /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 feea0dec1c..f9ea21f4a8 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 { - pub(super) panes: HashMap, - pub(super) internal: Internal, + /// The panes of the [`PaneGrid`]. + pub panes: HashMap, + + /// The internal state of the [`PaneGrid`]. + pub internal: Internal, + + pub(super) action: Action, } impl State { @@ -39,16 +45,13 @@ impl State { pub fn with_configuration(config: impl Into>) -> 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 State { 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( panes: &mut HashMap, content: Configuration, 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 State { (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 { 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/pure/src/widget.rs b/pure/src/widget.rs index 8f2cf92025..564f058341 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,4 +1,5 @@ pub mod image; +pub mod pane_grid; pub mod progress_bar; pub mod rule; pub mod tree; @@ -24,6 +25,7 @@ pub use column::Column; pub use container::Container; pub use element::Element; pub use image::Image; +pub use pane_grid::PaneGrid; pub use pick_list::PickList; pub use progress_bar::ProgressBar; pub use radio::Radio; diff --git a/pure/src/widget/pane_grid.rs b/pure/src/widget/pane_grid.rs new file mode 100644 index 0000000000..717c9ceb3a --- /dev/null +++ b/pure/src/widget/pane_grid.rs @@ -0,0 +1,399 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. +/// +/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](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, 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 Message + 'a>>, + on_drag: Option Message + 'a>>, + on_resize: Option<(u16, Box Message + 'a>)>, + style_sheet: Box, +} + +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( + state: &'a State, + 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(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(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(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>) -> Self { + self.style_sheet = style.into(); + self + } +} + +impl<'a, Message, Renderer> Widget + for PaneGrid<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) + } + + fn children(&self) -> Vec { + 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::(); + + 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> { + 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> + 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 0000000000..a928b28c06 --- /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>, + body: Element<'a, Message, Renderer>, + style_sheet: Box, +} + +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>) -> 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>, + ) -> 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> { + 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 for Content<'a, Message, Renderer> +where + T: Into>, + 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 0000000000..dd68b0734a --- /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>, + padding: Padding, + always_show_controls: bool, + style_sheet: Box, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`TitleBar`] with the given content. + pub fn new(content: E) -> Self + where + E: Into>, + { + 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>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn 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>, + ) -> 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> { + 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/tree.rs b/pure/src/widget/tree.rs index 33f5693a01..3fcf0922ba 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -41,6 +41,19 @@ impl Tree { pub fn diff_children( &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( + &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()); @@ -49,12 +62,12 @@ impl Tree { for (child_state, new) in self.children.iter_mut().zip(new_children.iter()) { - child_state.diff(new); + diff(new, child_state); } if self.children.len() < new_children.len() { self.children.extend( - new_children[self.children.len()..].iter().map(Self::new), + new_children[self.children.len()..].iter().map(new_state), ); } } From cdd906f563de21dd904bf5b78589d8a999b46931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 14 Mar 2022 17:43:38 +0700 Subject: [PATCH 49/55] Implement `pure` version of `pane_grid` example :tada: --- examples/pure/pane_grid/src/main.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/pure/pane_grid/src/main.rs b/examples/pure/pane_grid/src/main.rs index 6232d6be9d..6a7330231d 100644 --- a/examples/pure/pane_grid/src/main.rs +++ b/examples/pure/pane_grid/src/main.rs @@ -13,7 +13,7 @@ pub fn main() -> iced::Result { } struct Example { - panes: pane_grid::Map, + panes: pane_grid::State, panes_created: usize, focus: Option, } @@ -37,7 +37,7 @@ impl Application for Example { type Flags = (); fn new(_flags: ()) -> (Self, Command) { - let (panes, _) = pane_grid::Map::new(Pane::new(0)); + let (panes, _) = pane_grid::State::new(Pane::new(0)); ( Example { @@ -154,21 +154,20 @@ impl Application for Example { let focus = self.focus; let total_panes = self.panes.len(); - let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| { + let pane_grid = PaneGrid::new(&self.panes, |id, pane| { let is_focused = focus == Some(id); - let Pane { id, is_pinned } = pane; - - let pin_button = - button(text(if *is_pinned { "Unpin" } else { "Pin" }).size(14)) - .on_press(Message::TogglePin(id)) - .style(style::Button::Pin) - .padding(3); + 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(id.to_string()).color(if is_focused { + .push(text(pane.id.to_string()).color(if is_focused { PANE_ID_COLOR_FOCUSED } else { PANE_ID_COLOR_UNFOCUSED @@ -176,7 +175,7 @@ impl Application for Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls(id, total_panes, *is_pinned)) + .controls(view_controls(id, total_panes, pane.is_pinned)) .padding(10) .style(if is_focused { style::TitleBar::Focused @@ -185,7 +184,7 @@ impl Application for Example { }); pane_grid::Content::new(responsive(move |size| { - view_content(id, total_panes, *is_pinned, size) + view_content(id, total_panes, pane.is_pinned, size) })) .title_bar(title_bar) .style(if is_focused { From d7100fd2597da82d97eaf196d50573ea64f3f8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 16 Mar 2022 17:37:19 +0700 Subject: [PATCH 50/55] Export widget modules in `iced_pure` ... and fix collisions with the new `helpers` --- examples/pure/component/Cargo.toml | 1 + examples/pure/component/src/main.rs | 8 +- examples/pure/counter/src/main.rs | 3 +- examples/pure/game_of_life/src/main.rs | 6 +- examples/pure/pane_grid/src/main.rs | 2 +- examples/pure/pick_list/src/main.rs | 4 +- examples/pure/todos/src/main.rs | 5 +- examples/pure/tour/src/main.rs | 5 +- graphics/src/widget/pure/canvas.rs | 2 +- lazy/src/pure/responsive.rs | 2 +- pure/src/{widget => }/element.rs | 0 pure/src/helpers.rs | 153 ++++++++++++++++++++++ pure/src/lib.rs | 7 +- pure/src/overlay.rs | 2 +- pure/src/widget.rs | 169 ++----------------------- pure/src/widget/button.rs | 7 +- pure/src/widget/checkbox.rs | 5 +- pure/src/widget/column.rs | 3 +- pure/src/widget/container.rs | 3 +- pure/src/widget/pane_grid.rs | 3 +- pure/src/widget/progress_bar.rs | 3 +- pure/src/widget/radio.rs | 5 +- pure/src/widget/row.rs | 3 +- pure/src/widget/rule.rs | 3 +- pure/src/widget/scrollable.rs | 2 +- pure/src/widget/space.rs | 3 +- pure/src/widget/text.rs | 3 +- pure/src/widget/text_input.rs | 4 +- pure/src/widget/tree.rs | 2 +- src/pure.rs | 37 +----- src/pure/widget.rs | 155 +++++++++++++++++++++++ 31 files changed, 378 insertions(+), 232 deletions(-) rename pure/src/{widget => }/element.rs (100%) create mode 100644 pure/src/helpers.rs create mode 100644 src/pure/widget.rs diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml index a15f134f2b..b6c7a5136a 100644 --- a/examples/pure/component/Cargo.toml +++ b/examples/pure/component/Cargo.toml @@ -9,3 +9,4 @@ publish = false 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 index ab10317f25..b38d6fca89 100644 --- a/examples/pure/component/src/main.rs +++ b/examples/pure/component/src/main.rs @@ -1,4 +1,4 @@ -use iced::pure::widget::container; +use iced::pure::container; use iced::pure::{Element, Sandbox}; use iced::{Length, Settings}; @@ -47,12 +47,12 @@ impl Sandbox for Component { } mod numeric_input { - use iced::pure::widget::Element; - use iced::pure::widget::{row, text, text_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 { value: Option, @@ -120,8 +120,6 @@ mod numeric_input { fn view(&self, _state: &Self::State) -> Element { let button = |label, on_press| { - use iced::pure::widget::button; - button( text(label) .width(Length::Fill) diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs index 9b5203472f..726009dfe2 100644 --- a/examples/pure/counter/src/main.rs +++ b/examples/pure/counter/src/main.rs @@ -1,5 +1,4 @@ -use iced::pure::widget::{button, column, text}; -use iced::pure::{Element, Sandbox}; +use iced::pure::{button, column, text, Element, Sandbox}; use iced::{Alignment, Settings}; pub fn main() -> iced::Result { diff --git a/examples/pure/game_of_life/src/main.rs b/examples/pure/game_of_life/src/main.rs index 75a0deef3c..f63ef41b91 100644 --- a/examples/pure/game_of_life/src/main.rs +++ b/examples/pure/game_of_life/src/main.rs @@ -5,7 +5,7 @@ mod style; use grid::Grid; use iced::executor; -use iced::pure::widget::{ +use iced::pure::{ button, checkbox, column, container, pick_list, row, slider, text, }; use iced::pure::{Application, Element}; @@ -207,8 +207,8 @@ fn view_controls<'a>( mod grid { use crate::Preset; - use iced::pure::canvas::event::{self, Event}; - use iced::pure::canvas::{ + 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; diff --git a/examples/pure/pane_grid/src/main.rs b/examples/pure/pane_grid/src/main.rs index 6a7330231d..6551695668 100644 --- a/examples/pure/pane_grid/src/main.rs +++ b/examples/pure/pane_grid/src/main.rs @@ -1,7 +1,7 @@ use iced::alignment::{self, Alignment}; use iced::executor; use iced::keyboard; -use iced::pure::pane_grid::{self, PaneGrid}; +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}; diff --git a/examples/pure/pick_list/src/main.rs b/examples/pure/pick_list/src/main.rs index f9d55dd092..b9947107c1 100644 --- a/examples/pure/pick_list/src/main.rs +++ b/examples/pure/pick_list/src/main.rs @@ -1,6 +1,4 @@ -use iced::pure::widget::{ - column, container, pick_list, scrollable, vertical_space, -}; +use iced::pure::{column, container, pick_list, scrollable, vertical_space}; use iced::pure::{Element, Sandbox}; use iced::{Alignment, Length, Settings}; diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs index e993c598bd..6a6c630019 100644 --- a/examples/pure/todos/src/main.rs +++ b/examples/pure/todos/src/main.rs @@ -1,8 +1,9 @@ use iced::alignment::{self, Alignment}; -use iced::pure::widget::{ +use iced::pure::widget::Text; +use iced::pure::{ button, checkbox, column, container, row, scrollable, text, text_input, + Application, Element, }; -use iced::pure::{Application, Element, Text}; use iced::window; use iced::{Command, Font, Length, Settings}; use serde::{Deserialize, Serialize}; diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs index 7a50bcdcd8..a44d99f366 100644 --- a/examples/pure/tour/src/main.rs +++ b/examples/pure/tour/src/main.rs @@ -1,9 +1,10 @@ use iced::alignment; -use iced::pure::widget::{ +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::{Button, Column, Container, Element, Sandbox, Slider}; +use iced::pure::{Element, Sandbox}; use iced::{Color, Length, Settings}; pub fn main() -> iced::Result { diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs index 912143925a..c48dea6c4a 100644 --- a/graphics/src/widget/pure/canvas.rs +++ b/graphics/src/widget/pure/canvas.rs @@ -16,7 +16,7 @@ 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::widget::{Element, Widget}; +use iced_pure::{Element, Widget}; /// A widget capable of drawing 2D graphics. /// diff --git a/lazy/src/pure/responsive.rs b/lazy/src/pure/responsive.rs index 291f1aaf6d..2b62a04702 100644 --- a/lazy/src/pure/responsive.rs +++ b/lazy/src/pure/responsive.rs @@ -3,8 +3,8 @@ 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::horizontal_space; use iced_pure::widget::tree::{self, Tree}; use iced_pure::{Element, Widget}; diff --git a/pure/src/widget/element.rs b/pure/src/element.rs similarity index 100% rename from pure/src/widget/element.rs rename to pure/src/element.rs diff --git a/pure/src/helpers.rs b/pure/src/helpers.rs new file mode 100644 index 0000000000..24f6dbaa43 --- /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>, +) -> 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>, +) -> widget::Scrollable<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + widget::Scrollable::new(content) +} + +pub fn button<'a, Message, Renderer>( + content: impl Into>, +) -> widget::Button<'a, Message, Renderer> { + widget::Button::new(content) +} + +pub fn text(text: impl Into) -> widget::Text +where + Renderer: iced_native::text::Renderer, +{ + widget::Text::new(text) +} + +pub fn checkbox<'a, Message, Renderer>( + label: impl Into, + 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, + value: V, + selected: Option, + 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>, + 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, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::Slider<'a, T, Message> +where + Message: Clone, + T: Copy + From + std::cmp::PartialOrd, +{ + widget::Slider::new(range, value, on_change) +} + +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into>, + selected: Option, + on_selected: impl Fn(T) -> Message + 'a, +) -> widget::PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned>, + Renderer: iced_native::text::Renderer, +{ + widget::PickList::new(options, selected, on_selected) +} + +pub fn image(handle: impl Into) -> widget::Image { + 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, + value: f32, +) -> widget::ProgressBar<'a> { + widget::ProgressBar::new(range, value) +} diff --git a/pure/src/lib.rs b/pure/src/lib.rs index 1b51d55b08..ec2f29f8af 100644 --- a/pure/src/lib.rs +++ b/pure/src/lib.rs @@ -1,9 +1,14 @@ +pub mod helpers; pub mod overlay; pub mod widget; pub(crate) mod flex; -pub use widget::*; +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}; diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs index 72415634ae..c87dfce8fd 100644 --- a/pure/src/overlay.rs +++ b/pure/src/overlay.rs @@ -1,4 +1,4 @@ -use crate::Tree; +use crate::widget::Tree; use iced_native::Layout; diff --git a/pure/src/widget.rs b/pure/src/widget.rs index 564f058341..be73c5fa56 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -1,29 +1,27 @@ +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 text_input; +pub mod toggler; pub mod tree; -mod button; -mod checkbox; mod column; -mod container; -mod element; -mod pick_list; -mod radio; mod row; -mod scrollable; -mod slider; mod space; mod text; -mod text_input; -mod toggler; pub use button::Button; pub use checkbox::Checkbox; pub use column::Column; pub use container::Container; -pub use element::Element; pub use image::Image; pub use pane_grid::PaneGrid; pub use pick_list::PickList; @@ -46,9 +44,6 @@ use iced_native::overlay; use iced_native::renderer; use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; -use std::borrow::Cow; -use std::ops::RangeInclusive; - pub trait Widget { fn width(&self) -> Length; @@ -117,149 +112,3 @@ pub trait Widget { None } } - -pub fn container<'a, Message, Renderer>( - content: impl Into>, -) -> Container<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - Container::new(content) -} - -pub fn column<'a, Message, Renderer>() -> Column<'a, Message, Renderer> { - Column::new() -} - -pub fn row<'a, Message, Renderer>() -> Row<'a, Message, Renderer> { - Row::new() -} - -pub fn scrollable<'a, Message, Renderer>( - content: impl Into>, -) -> Scrollable<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - Scrollable::new(content) -} - -pub fn button<'a, Message, Renderer>( - content: impl Into>, -) -> Button<'a, Message, Renderer> { - Button::new(content) -} - -pub fn text(text: impl Into) -> Text -where - Renderer: iced_native::text::Renderer, -{ - Text::new(text) -} - -pub fn checkbox<'a, Message, Renderer>( - label: impl Into, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> Checkbox<'a, Message, Renderer> -where - Renderer: iced_native::text::Renderer, -{ - Checkbox::new(is_checked, label, f) -} - -pub fn radio<'a, Message, Renderer, V>( - label: impl Into, - value: V, - selected: Option, - on_click: impl FnOnce(V) -> Message, -) -> Radio<'a, Message, Renderer> -where - Message: Clone, - Renderer: iced_native::text::Renderer, - V: Copy + Eq, -{ - Radio::new(value, label, selected, on_click) -} - -pub fn toggler<'a, Message, Renderer>( - label: impl Into>, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> Toggler<'a, Message, Renderer> -where - Renderer: iced_native::text::Renderer, -{ - Toggler::new(is_checked, label, f) -} - -pub fn text_input<'a, Message, Renderer>( - placeholder: &str, - value: &str, - on_change: impl Fn(String) -> Message + 'a, -) -> TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: iced_native::text::Renderer, -{ - TextInput::new(placeholder, value, on_change) -} - -pub fn slider<'a, Message, T>( - range: std::ops::RangeInclusive, - value: T, - on_change: impl Fn(T) -> Message + 'a, -) -> Slider<'a, T, Message> -where - Message: Clone, - T: Copy + From + std::cmp::PartialOrd, -{ - Slider::new(range, value, on_change) -} - -pub fn pick_list<'a, Message, Renderer, T>( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, -) -> PickList<'a, T, Message, Renderer> -where - T: ToString + Eq + 'static, - [T]: ToOwned>, - Renderer: iced_native::text::Renderer, -{ - PickList::new(options, selected, on_selected) -} - -pub fn image(handle: impl Into) -> Image { - Image::new(handle.into()) -} - -pub fn horizontal_space(width: Length) -> Space { - Space::with_width(width) -} - -pub fn vertical_space(height: Length) -> Space { - Space::with_height(height) -} - -/// Creates a horizontal [`Rule`] with the given height. -pub fn horizontal_rule<'a>(height: u16) -> Rule<'a> { - Rule::horizontal(height) -} - -/// Creates a vertical [`Rule`] with the given width. -pub fn vertical_rule<'a>(width: u16) -> Rule<'a> { - 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, - value: f32, -) -> ProgressBar<'a> { - ProgressBar::new(range, value) -} diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs index 4380b608e1..f99d301889 100644 --- a/pure/src/widget/button.rs +++ b/pure/src/widget/button.rs @@ -1,6 +1,6 @@ use crate::overlay; use crate::widget::tree::{self, Tree}; -use crate::widget::{Element, Widget}; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout; @@ -10,9 +10,10 @@ use iced_native::widget::button; use iced_native::{ Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, }; -use iced_style::button::StyleSheet; -pub use button::State; +pub use iced_style::button::{Style, StyleSheet}; + +use button::State; pub struct Button<'a, Message, Renderer> { content: Element<'a, Message, Renderer>, diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs index 3448e6161f..971980e31a 100644 --- a/pure/src/widget/checkbox.rs +++ b/pure/src/widget/checkbox.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; @@ -7,7 +8,7 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; -pub use iced_native::widget::Checkbox; +pub use iced_native::widget::checkbox::{Checkbox, Style, StyleSheet}; impl<'a, Message, Renderer> Widget for Checkbox<'a, Message, Renderer> diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs index 37ff96c5b8..6b4472706f 100644 --- a/pure/src/widget/column.rs +++ b/pure/src/widget/column.rs @@ -1,6 +1,7 @@ use crate::flex; use crate::overlay; -use crate::widget::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs index ebf69cabcb..91db1f3f38 100644 --- a/pure/src/widget/container.rs +++ b/pure/src/widget/container.rs @@ -1,5 +1,6 @@ //! Decorate content and apply alignment. -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::alignment; use iced_native::event::{self, Event}; diff --git a/pure/src/widget/pane_grid.rs b/pure/src/widget/pane_grid.rs index 717c9ceb3a..34a56bcc49 100644 --- a/pure/src/widget/pane_grid.rs +++ b/pure/src/widget/pane_grid.rs @@ -54,7 +54,8 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// ## Example /// /// ``` -/// # use iced_pure::widget::{pane_grid, text}; +/// # use iced_pure::widget::pane_grid; +/// # use iced_pure::text; /// # /// # type PaneGrid<'a, Message> = /// # iced_pure::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; diff --git a/pure/src/widget/progress_bar.rs b/pure/src/widget/progress_bar.rs index 9b996f0250..3f4ffd554c 100644 --- a/pure/src/widget/progress_bar.rs +++ b/pure/src/widget/progress_bar.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs index ce3ede8416..c20f8f3eaa 100644 --- a/pure/src/widget/radio.rs +++ b/pure/src/widget/radio.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; @@ -7,7 +8,7 @@ use iced_native::renderer; use iced_native::text; use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; -pub use iced_native::widget::Radio; +pub use iced_native::widget::radio::{Radio, Style, StyleSheet}; impl<'a, Message, Renderer> Widget for Radio<'a, Message, Renderer> diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs index fa0efa68f0..d7f90540ba 100644 --- a/pure/src/widget/row.rs +++ b/pure/src/widget/row.rs @@ -1,6 +1,7 @@ use crate::flex; use crate::overlay; -use crate::widget::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; diff --git a/pure/src/widget/rule.rs b/pure/src/widget/rule.rs index 375bed9efa..40b1fc90d8 100644 --- a/pure/src/widget/rule.rs +++ b/pure/src/widget/rule.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs index 1548fa9d3a..f9a512008b 100644 --- a/pure/src/widget/scrollable.rs +++ b/pure/src/widget/scrollable.rs @@ -9,7 +9,7 @@ use iced_native::renderer; use iced_native::widget::scrollable; use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Vector}; -pub use iced_style::scrollable::StyleSheet; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs index c04d962af8..b408153be7 100644 --- a/pure/src/widget/space.rs +++ b/pure/src/widget/space.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index bfcbaa4b8e..edc35cd17c 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -1,4 +1,5 @@ -use crate::{Element, Tree, Widget}; +use crate::widget::Tree; +use crate::{Element, Widget}; use iced_native::layout::{self, Layout}; use iced_native::renderer; diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs index dec11164fb..d6041d7fc7 100644 --- a/pure/src/widget/text_input.rs +++ b/pure/src/widget/text_input.rs @@ -1,5 +1,5 @@ use crate::widget::tree::{self, Tree}; -use crate::widget::{Element, Widget}; +use crate::{Element, Widget}; use iced_native::event::{self, Event}; use iced_native::layout::{self, Layout}; @@ -9,7 +9,7 @@ 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::StyleSheet; +pub use iced_style::text_input::{Style, StyleSheet}; /// A field that can be filled with text. /// diff --git a/pure/src/widget/tree.rs b/pure/src/widget/tree.rs index 3fcf0922ba..bd7c259c86 100644 --- a/pure/src/widget/tree.rs +++ b/pure/src/widget/tree.rs @@ -1,4 +1,4 @@ -use crate::widget::Element; +use crate::Element; use std::any::{self, Any}; diff --git a/src/pure.rs b/src/pure.rs index f3f73bba2e..948183f159 100644 --- a/src/pure.rs +++ b/src/pure.rs @@ -17,33 +17,7 @@ //! [the original widgets]: crate::widget //! [`button::State`]: crate::widget::button::State //! [impure `Application`]: crate::Application -pub use iced_pure::{ - Button as _, Column as _, Element as _, Image as _, Row as _, Text as _, *, -}; - -/// A generic, pure [`Widget`]. -pub type Element<'a, Message> = - iced_pure::Element<'a, Message, crate::Renderer>; - -/// A pure container widget. -pub type Container<'a, Message> = - iced_pure::Container<'a, Message, crate::Renderer>; - -/// A pure column widget. -pub type Column<'a, Message> = iced_pure::Column<'a, Message, crate::Renderer>; - -/// A pure row widget. -pub type Row<'a, Message> = iced_pure::Row<'a, Message, crate::Renderer>; - -/// A pure button widget. -pub type Button<'a, Message> = iced_pure::Button<'a, Message, crate::Renderer>; - -/// A pure text widget. -pub type Text = iced_pure::Text; - -#[cfg(feature = "image")] -/// A pure image widget. -pub type Image = iced_pure::Image; +pub mod widget; mod application; mod sandbox; @@ -51,8 +25,9 @@ mod sandbox; pub use application::Application; pub use sandbox::Sandbox; -#[cfg(feature = "canvas")] -pub use iced_graphics::widget::pure::canvas; +pub use iced_pure::helpers::*; +pub use iced_pure::{Pure, State}; -#[cfg(feature = "canvas")] -pub use canvas::Canvas; +/// A generic, pure [`Widget`]. +pub type Element<'a, Message> = + iced_pure::Element<'a, Message, crate::Renderer>; diff --git a/src/pure/widget.rs b/src/pure/widget.rs new file mode 100644 index 0000000000..e9a533ea57 --- /dev/null +++ b/src/pure/widget.rs @@ -0,0 +1,155 @@ +//! 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; + +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. + //! + //! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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. + /// + /// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](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 = "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; +} + +#[cfg(feature = "canvas")] +pub use canvas::Canvas; + +#[cfg(feature = "image")] +pub use image::Image; From 32fd8dadda6636b11d4a1d9f59b467e57b3706a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Mar 2022 22:13:52 +0700 Subject: [PATCH 51/55] Reintroduce generic `Message` type for `canvas::Program` As it is useful to make the `Message` completely free in many implementations. --- examples/bezier_tool/src/main.rs | 4 +-- examples/clock/src/main.rs | 4 +-- examples/color_palette/src/main.rs | 4 +-- examples/game_of_life/src/main.rs | 4 +-- examples/pure/game_of_life/src/main.rs | 3 +- examples/solar_system/src/main.rs | 4 +-- graphics/src/widget/canvas.rs | 41 ++++++++++++---------- graphics/src/widget/canvas/program.rs | 15 +++----- graphics/src/widget/pure/canvas.rs | 30 +++++++++------- graphics/src/widget/pure/canvas/program.rs | 14 +++----- 10 files changed, 55 insertions(+), 68 deletions(-) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fc7ef0c918..35b5182c61 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -104,9 +104,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { - type Message = Curve; - + impl<'a> canvas::Program for Bezier<'a> { fn update( &mut self, event: Event, diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 41d160c15e..3b8a1d6ad5 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -76,9 +76,7 @@ impl Application for Clock { } } -impl canvas::Program for Clock { - type Message = Message; - +impl canvas::Program for Clock { fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { 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 c9c0637ec7..5dbbd8f3bc 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -235,9 +235,7 @@ impl Theme { } } -impl canvas::Program for Theme { - type Message = Message; - +impl canvas::Program for Theme { fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { let theme = self.canvas_cache.draw(bounds.size(), |frame| { self.draw(frame); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 43ef8ffddb..ab8b80e4e1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -328,9 +328,7 @@ mod grid { } } - impl<'a> canvas::Program for Grid { - type Message = Message; - + impl<'a> canvas::Program for Grid { fn update( &mut self, event: Event, diff --git a/examples/pure/game_of_life/src/main.rs b/examples/pure/game_of_life/src/main.rs index f63ef41b91..a316470152 100644 --- a/examples/pure/game_of_life/src/main.rs +++ b/examples/pure/game_of_life/src/main.rs @@ -394,8 +394,7 @@ mod grid { } } - impl canvas::Program for Grid { - type Message = Message; + impl canvas::Program for Grid { type State = Interaction; fn update( diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index bf96fee0c8..12184dd19d 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -128,9 +128,7 @@ impl State { } } -impl canvas::Program for State { - type Message = Message; - +impl canvas::Program for State { fn draw( &self, bounds: Rectangle, diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index ced2ce53a3..6c526e356d 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -6,13 +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, -}; - pub mod event; pub mod path; @@ -36,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 @@ -72,9 +74,7 @@ pub use text::Text; /// } /// /// // Then, we implement the `Program` trait -/// impl Program for Circle { -/// type Message = (); -/// +/// impl Program<()> for Circle { /// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(bounds.size()); @@ -94,13 +94,14 @@ pub use text::Text; /// let canvas = Canvas::new(Circle { radius: 50.0 }); /// ``` #[derive(Debug)] -pub struct Canvas { +pub struct Canvas> { width: Length, height: Length, program: P, + message_: PhantomData, } -impl Canvas

{ +impl> Canvas { const DEFAULT_SIZE: u16 = 100; /// Creates a new [`Canvas`]. @@ -109,6 +110,7 @@ impl Canvas

{ width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), program, + message_: PhantomData, } } @@ -125,9 +127,9 @@ impl Canvas

{ } } -impl Widget> for Canvas

+impl Widget> for Canvas where - P: Program, + P: Program, B: Backend, { fn width(&self) -> Length { @@ -156,7 +158,7 @@ where cursor_position: Point, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, P::Message>, + shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); @@ -231,13 +233,14 @@ where } } -impl<'a, P, B> From> for Element<'a, P::Message, Renderer> +impl<'a, Message, P, B> From> + for Element<'a, Message, Renderer> where - P::Message: 'static, - P: Program + 'a, + Message: 'static, + P: Program + 'a, B: Backend, { - fn from(canvas: Canvas

) -> Element<'a, P::Message, Renderer> { + fn from(canvas: Canvas) -> Element<'a, Message, Renderer> { Element::new(canvas) } } diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index f8b9ff2b69..85a2f67b30 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -8,10 +8,7 @@ use iced_native::{mouse, Rectangle}; /// application. /// /// [`Canvas`]: crate::widget::Canvas -pub trait Program { - /// The [`Message`] produced by the [`Program`]. - type Message; - +pub trait Program { /// Updates the state of the [`Program`]. /// /// When a [`Program`] is used in a [`Canvas`], the runtime will call this @@ -28,7 +25,7 @@ pub trait Program { _event: Event, _bounds: Rectangle, _cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { (event::Status::Ignored, None) } @@ -56,18 +53,16 @@ pub trait Program { } } -impl Program for &mut T +impl Program for &mut T where - T: Program, + T: Program, { - type Message = T::Message; - fn update( &mut self, event: Event, bounds: Rectangle, cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { T::update(self, event, bounds, cursor) } diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs index c48dea6c4a..2e3e7ede07 100644 --- a/graphics/src/widget/pure/canvas.rs +++ b/graphics/src/widget/pure/canvas.rs @@ -18,6 +18,8 @@ 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 @@ -40,8 +42,7 @@ use iced_pure::{Element, Widget}; /// } /// /// // Then, we implement the `Program` trait -/// impl Program for Circle { -/// type Message = (); +/// impl Program<()> for Circle { /// type State = (); /// /// fn draw(&self, _state: &(), bounds: Rectangle, _cursor: Cursor) -> Vec{ @@ -63,18 +64,19 @@ use iced_pure::{Element, Widget}; /// let canvas = Canvas::new(Circle { radius: 50.0 }); /// ``` #[derive(Debug)] -pub struct Canvas

+pub struct Canvas where - P: Program, + P: Program, { width: Length, height: Length, program: P, + message_: PhantomData, } -impl

Canvas

+impl Canvas where - P: Program, + P: Program, { const DEFAULT_SIZE: u16 = 100; @@ -84,6 +86,7 @@ where width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), program, + message_: PhantomData, } } @@ -100,9 +103,9 @@ where } } -impl Widget> for Canvas

+impl Widget> for Canvas where - P: Program, + P: Program, B: Backend, { fn tag(&self) -> tree::Tag { @@ -140,7 +143,7 @@ where cursor_position: Point, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, P::Message>, + shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); @@ -221,13 +224,14 @@ where } } -impl<'a, P, B> From> for Element<'a, P::Message, Renderer> +impl<'a, Message, P, B> From> + for Element<'a, Message, Renderer> where - P::Message: 'static, - P: Program + 'a, + Message: 'a, + P: Program + 'a, B: Backend, { - fn from(canvas: Canvas

) -> Element<'a, P::Message, Renderer> { + fn from(canvas: Canvas) -> Element<'a, Message, Renderer> { Element::new(canvas) } } diff --git a/graphics/src/widget/pure/canvas/program.rs b/graphics/src/widget/pure/canvas/program.rs index cb52910dc7..ee74c27f91 100644 --- a/graphics/src/widget/pure/canvas/program.rs +++ b/graphics/src/widget/pure/canvas/program.rs @@ -9,10 +9,7 @@ use crate::Rectangle; /// application. /// /// [`Canvas`]: crate::widget::Canvas -pub trait Program { - /// The [`Message`] produced by the [`Program`]. - type Message; - +pub trait Program { /// The internal [`State`] mutated by the [`Program`]. type State: Default + 'static; @@ -33,7 +30,7 @@ pub trait Program { _event: Event, _bounds: Rectangle, _cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { (event::Status::Ignored, None) } @@ -67,11 +64,10 @@ pub trait Program { } } -impl Program for &T +impl Program for &T where - T: Program, + T: Program, { - type Message = T::Message; type State = T::State; fn update( @@ -80,7 +76,7 @@ where event: Event, bounds: Rectangle, cursor: Cursor, - ) -> (event::Status, Option) { + ) -> (event::Status, Option) { T::update(self, state, event, bounds, cursor) } From 497a3ca8abb99f2bf25083a51388146433fe90a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Mar 2022 22:22:19 +0700 Subject: [PATCH 52/55] Restore `TextInput::draw` helper This helper should be unnecessary in the future. --- native/src/widget/text_input.rs | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index d13d6ef118..057f34c68c 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -154,6 +154,29 @@ where pub fn state(&self) -> &State { self.state } + + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) { + 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(), + ) + } } /// Computes the layout of a [`TextInput`]. @@ -791,18 +814,7 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor_position, - &self.state, - &self.value, - &self.placeholder, - self.size, - &self.font, - self.is_secure, - self.style_sheet.as_ref(), - ) + self.draw(renderer, layout, cursor_position, None) } } From 989c56292096e512c325a745013c5da86066d0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 21 Mar 2022 17:22:46 +0700 Subject: [PATCH 53/55] Implement `pure` version of `QRCode` widget --- graphics/src/widget/pure.rs | 6 +++ graphics/src/widget/pure/qr_code.rs | 61 +++++++++++++++++++++++++++++ src/pure/widget.rs | 6 +++ 3 files changed, 73 insertions(+) create mode 100644 graphics/src/widget/pure/qr_code.rs diff --git a/graphics/src/widget/pure.rs b/graphics/src/widget/pure.rs index 3ecbadf1c6..ee530379db 100644 --- a/graphics/src/widget/pure.rs +++ b/graphics/src/widget/pure.rs @@ -4,3 +4,9 @@ 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/qr_code.rs b/graphics/src/widget/pure/qr_code.rs new file mode 100644 index 0000000000..9d5173749d --- /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> for QRCode<'a> +where + B: Backend, +{ + fn width(&self) -> Length { + >>::width(self) + } + + fn height(&self) -> Length { + >>::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >>::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + >>::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, B> Into>> for QRCode<'a> +where + B: Backend, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/src/pure/widget.rs b/src/pure/widget.rs index e9a533ea57..39ccd09000 100644 --- a/src/pure/widget.rs +++ b/src/pure/widget.rs @@ -139,6 +139,9 @@ 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. @@ -151,5 +154,8 @@ pub mod image { #[cfg(feature = "canvas")] pub use canvas::Canvas; +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; + #[cfg(feature = "image")] pub use image::Image; From 9157f5b9e47713d5920a4e262c25a993998b312f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Mar 2022 13:27:12 +0700 Subject: [PATCH 54/55] Use application lifetime in `Into` implementation for `&str` --- pure/src/widget/text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs index edc35cd17c..b78d41177d 100644 --- a/pure/src/widget/text.rs +++ b/pure/src/widget/text.rs @@ -60,8 +60,7 @@ where } } -impl<'a, Message, Renderer> Into> - for &'static str +impl<'a, Message, Renderer> Into> for &'a str where Renderer: text::Renderer + 'static, { From ef4c79ea23e86fec9a8ad0fb27463296c14400e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 22 Mar 2022 23:40:08 +0700 Subject: [PATCH 55/55] Implement `pure` version of `Svg` widget --- pure/src/widget.rs | 2 ++ pure/src/widget/svg.rs | 62 ++++++++++++++++++++++++++++++++++++++++++ src/pure/widget.rs | 6 ++++ 3 files changed, 70 insertions(+) create mode 100644 pure/src/widget/svg.rs diff --git a/pure/src/widget.rs b/pure/src/widget.rs index be73c5fa56..8200f9a752 100644 --- a/pure/src/widget.rs +++ b/pure/src/widget.rs @@ -9,6 +9,7 @@ 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; @@ -32,6 +33,7 @@ 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; diff --git a/pure/src/widget/svg.rs b/pure/src/widget/svg.rs new file mode 100644 index 0000000000..2758c5b176 --- /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 Widget for Svg +where + Renderer: iced_native::svg::Renderer, +{ + fn width(&self) -> Length { + >::width(self) + } + + fn height(&self) -> Length { + >::height(self) + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + >::layout( + self, renderer, limits, + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + >::draw( + self, + renderer, + style, + layout, + cursor_position, + viewport, + ) + } +} + +impl<'a, Message, Renderer> Into> 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/src/pure/widget.rs b/src/pure/widget.rs index 39ccd09000..6628b1fb68 100644 --- a/src/pure/widget.rs +++ b/src/pure/widget.rs @@ -151,6 +151,9 @@ pub mod image { pub type Image = iced_pure::widget::Image; } +#[cfg(feature = "svg")] +pub use iced_pure::widget::svg; + #[cfg(feature = "canvas")] pub use canvas::Canvas; @@ -159,3 +162,6 @@ pub use qr_code::QRCode; #[cfg(feature = "image")] pub use image::Image; + +#[cfg(feature = "svg")] +pub use svg::Svg;