diff --git a/Cargo.toml b/Cargo.toml index 2f6727eb94..bff0eb7bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,6 @@ async-std = ["iced_futures/async-std"] smol = ["iced_futures/smol"] # Enables advanced color conversion via `palette` palette = ["iced_core/palette"] -# Enables pure, virtual widgets in the `pure` module -pure = ["iced_pure", "iced_graphics/pure"] # Enables querying system information system = ["iced_winit/system"] @@ -57,10 +55,10 @@ members = [ "glutin", "lazy", "native", - "pure", "style", "wgpu", "winit", + "examples/arc", "examples/bezier_tool", "examples/clock", "examples/color_palette", @@ -90,16 +88,6 @@ members = [ "examples/tour", "examples/url_handler", "examples/websocket", - "examples/pure/arc", - "examples/pure/color_palette", - "examples/pure/component", - "examples/pure/counter", - "examples/pure/game_of_life", - "examples/pure/pane_grid", - "examples/pure/pick_list", - "examples/pure/todos", - "examples/pure/tooltip", - "examples/pure/tour", ] [dependencies] @@ -110,7 +98,6 @@ iced_graphics = { version = "0.3", path = "graphics" } iced_winit = { version = "0.4", path = "winit" } iced_glutin = { version = "0.3", path = "glutin", optional = true } iced_glow = { version = "0.3", path = "glow", optional = true } -iced_pure = { version = "0.2", path = "pure", optional = true } thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/README.md b/README.md index abb5b0375f..5e52234e6c 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,9 @@ that can be incremented and decremented using two buttons. We start by modelling the __state__ of our application: ```rust -use iced::button; - struct Counter { // The counter value value: i32, - - // The local state of the two buttons - increment_button: button::State, - decrement_button: button::State, } ``` @@ -125,28 +119,23 @@ Now, let's show the actual counter by putting it all together in our __view logic__: ```rust -use iced::{Button, Column, Text}; +use iced::widget::{button, column, text, Column}; impl Counter { - pub fn view(&mut self) -> Column { + pub fn view(&self) -> Column { // We use a column: a simple vertical layout - Column::new() - .push( - // The increment button. We tell it to produce an - // `IncrementPressed` message when pressed - Button::new(&mut self.increment_button, Text::new("+")) - .on_press(Message::IncrementPressed), - ) - .push( - // We show the value of the counter here - Text::new(self.value.to_string()).size(50), - ) - .push( - // The decrement button. We tell it to produce a - // `DecrementPressed` message when pressed - Button::new(&mut self.decrement_button, Text::new("-")) - .on_press(Message::DecrementPressed), - ) + column![ + // The increment button. We tell it to produce an + // `IncrementPressed` message when pressed + button("+").on_press(Message::IncrementPressed), + + // We show the value of the counter here + text(self.value).size(50), + + // The decrement button. We tell it to produce a + // `DecrementPressed` message when pressed + button("-").on_press(Message::DecrementPressed), + ] } } ``` diff --git a/examples/pure/arc/Cargo.toml b/examples/arc/Cargo.toml similarity index 63% rename from examples/pure/arc/Cargo.toml rename to examples/arc/Cargo.toml index 22113cf1cb..e6e7436321 100644 --- a/examples/pure/arc/Cargo.toml +++ b/examples/arc/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../../..", features = ["pure", "canvas", "tokio", "debug"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } diff --git a/examples/pure/arc/README.md b/examples/arc/README.md similarity index 100% rename from examples/pure/arc/README.md rename to examples/arc/README.md diff --git a/examples/pure/arc/src/main.rs b/examples/arc/src/main.rs similarity index 94% rename from examples/pure/arc/src/main.rs rename to examples/arc/src/main.rs index df0e1e8a2c..0c619dc9ba 100644 --- a/examples/pure/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -1,11 +1,13 @@ use std::{f32::consts::PI, time::Instant}; use iced::executor; -use iced::pure::widget::canvas::{ +use iced::widget::canvas::{ self, Cache, Canvas, Cursor, Geometry, Path, Stroke, }; -use iced::pure::{Application, Element}; -use iced::{Command, Length, Point, Rectangle, Settings, Subscription, Theme}; +use iced::{ + Application, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, +}; pub fn main() -> iced::Result { Arc::run(Settings { diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 11e4828e4e..7c3916d4a2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,7 +1,6 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::{ - button, Alignment, Button, Column, Element, Length, Sandbox, Settings, Text, -}; +use iced::widget::{button, column, text}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings { @@ -14,7 +13,6 @@ pub fn main() -> iced::Result { struct Example { bezier: bezier::State, curves: Vec, - button_state: button::State, } #[derive(Debug, Clone, Copy)] @@ -47,44 +45,34 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .spacing(20) - .align_items(Alignment::Center) - .push( - Text::new("Bezier tool example") - .width(Length::Shrink) - .size(50), - ) - .push(self.bezier.view(&self.curves).map(Message::AddCurve)) - .push( - Button::new(&mut self.button_state, Text::new("Clear")) - .padding(8) - .on_press(Message::Clear), - ) - .into() + fn view(&self) -> Element { + column![ + text("Bezier tool example").width(Length::Shrink).size(50), + self.bezier.view(&self.curves).map(Message::AddCurve), + button("Clear").padding(8).on_press(Message::Clear), + ] + .padding(20) + .spacing(20) + .align_items(Alignment::Center) + .into() } } mod bezier { - use iced::{ - canvas::event::{self, Event}, - canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke}, - mouse, Element, Length, Point, Rectangle, Theme, + use iced::mouse; + use iced::widget::canvas::event::{self, Event}; + use iced::widget::canvas::{ + self, Canvas, Cursor, Frame, Geometry, Path, Stroke, }; + use iced::{Element, Length, Point, Rectangle, Theme}; #[derive(Default)] pub struct State { - pending: Option, cache: canvas::Cache, } impl State { - pub fn view<'a>( - &'a mut self, - curves: &'a [Curve], - ) -> Element<'a, Curve> { + pub fn view<'a>(&'a self, curves: &'a [Curve]) -> Element<'a, Curve> { Canvas::new(Bezier { state: self, curves, @@ -100,13 +88,16 @@ mod bezier { } struct Bezier<'a> { - state: &'a mut State, + state: &'a State, curves: &'a [Curve], } impl<'a> canvas::Program for Bezier<'a> { + type State = Option; + fn update( - &mut self, + &self, + state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, @@ -122,16 +113,16 @@ mod bezier { Event::Mouse(mouse_event) => { let message = match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { - match self.state.pending { + match *state { None => { - self.state.pending = Some(Pending::One { + *state = Some(Pending::One { from: cursor_position, }); None } Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { + *state = Some(Pending::Two { from, to: cursor_position, }); @@ -139,7 +130,7 @@ mod bezier { None } Some(Pending::Two { from, to }) => { - self.state.pending = None; + *state = None; Some(Curve { from, @@ -160,6 +151,7 @@ mod bezier { fn draw( &self, + state: &Self::State, _theme: &Theme, bounds: Rectangle, cursor: Cursor, @@ -170,11 +162,11 @@ mod bezier { frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default(), + Stroke::default().with_width(2.0), ); }); - if let Some(pending) = &self.state.pending { + if let Some(pending) = state { let pending_curve = pending.draw(bounds, cursor); vec![content, pending_curve] @@ -185,6 +177,7 @@ mod bezier { fn mouse_interaction( &self, + _state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 48b4cd7bf4..8818fb54ed 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,10 +1,9 @@ -use iced::canvas::{ - self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke, -}; use iced::executor; +use iced::widget::canvas::{Cache, Cursor, Geometry, LineCap, Path, Stroke}; +use iced::widget::{canvas, container}; use iced::{ - Application, Color, Command, Container, Element, Length, Point, Rectangle, - Settings, Subscription, Theme, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, Vector, }; pub fn main() -> iced::Result { @@ -69,10 +68,12 @@ impl Application for Clock { }) } - fn view(&mut self) -> Element { - let canvas = Canvas::new(self).width(Length::Fill).height(Length::Fill); + fn view(&self) -> Element { + let canvas = canvas(self as &Self) + .width(Length::Fill) + .height(Length::Fill); - Container::new(canvas) + container(canvas) .width(Length::Fill) .height(Length::Fill) .padding(20) @@ -81,8 +82,11 @@ impl Application for Clock { } impl canvas::Program for Clock { + type State = (); + fn draw( &self, + _state: &Self::State, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md index 95a23f48d0..f90020b1d6 100644 --- a/examples/color_palette/README.md +++ b/examples/color_palette/README.md @@ -11,5 +11,5 @@ A color palette generator, based on a user-defined root color. You can run it with `cargo run`: ``` -cargo run --package color_palette +cargo run --package pure_color_palette ``` diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index cc1af750a3..421499655b 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,7 +1,8 @@ -use iced::canvas::{self, Cursor, Frame, Geometry, Path}; +use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}; +use iced::widget::{column, row, text, Slider}; use iced::{ - alignment, slider, Alignment, Canvas, Color, Column, Element, Length, - Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector, + alignment, Alignment, Color, Element, Length, Point, Rectangle, Sandbox, + Settings, Size, Vector, }; use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; @@ -59,7 +60,7 @@ impl Sandbox for ColorPalette { self.theme = Theme::new(srgb); } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let base = self.theme.base; let srgb = palette::Srgb::from(base); @@ -69,17 +70,18 @@ impl Sandbox for ColorPalette { let lab = palette::Lab::from_color(srgb); let lch = palette::Lch::from_color(srgb); - Column::new() - .padding(10) - .spacing(10) - .push(self.rgb.view(base).map(Message::RgbColorChanged)) - .push(self.hsl.view(hsl).map(Message::HslColorChanged)) - .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) - .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) - .push(self.lab.view(lab).map(Message::LabColorChanged)) - .push(self.lch.view(lch).map(Message::LchColorChanged)) - .push(self.theme.view()) - .into() + column![ + self.rgb.view(base).map(Message::RgbColorChanged), + self.hsl.view(hsl).map(Message::HslColorChanged), + self.hsv.view(hsv).map(Message::HsvColorChanged), + self.hwb.view(hwb).map(Message::HwbColorChanged), + self.lab.view(lab).map(Message::LabColorChanged), + self.lch.view(lch).map(Message::LchColorChanged), + self.theme.view(), + ] + .padding(10) + .spacing(10) + .into() } } @@ -139,7 +141,7 @@ impl Theme { .chain(self.higher.iter()) } - pub fn view(&mut self) -> Element { + pub fn view(&self) -> Element { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -236,8 +238,11 @@ impl Theme { } impl canvas::Program for Theme { + type State = (); + fn draw( &self, + _state: &Self::State, _theme: &iced::Theme, bounds: Rectangle, _cursor: Cursor, @@ -267,7 +272,6 @@ fn color_hex_string(color: &Color) -> String { #[derive(Default)] struct ColorPicker { - sliders: [slider::State; 3], color_space: PhantomData, } @@ -282,37 +286,30 @@ trait ColorSpace: Sized { fn to_string(&self) -> String; } -impl ColorPicker { - fn view(&mut self, color: C) -> Element { +impl ColorPicker { + fn view(&self, color: C) -> Element { let [c1, c2, c3] = color.components(); - let [s1, s2, s3] = &mut self.sliders; let [cr1, cr2, cr3] = C::COMPONENT_RANGES; - fn slider( - state: &mut slider::State, + fn slider<'a, C: Clone>( range: RangeInclusive, component: f32, - update: impl Fn(f32) -> C + 'static, - ) -> Slider { - Slider::new(state, range, f64::from(component), move |v| { - update(v as f32) - }) - .step(0.01) + update: impl Fn(f32) -> C + 'a, + ) -> Slider<'a, f64, C, iced::Renderer> { + Slider::new(range, f64::from(component), move |v| update(v as f32)) + .step(0.01) } - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new(C::LABEL).width(Length::Units(50))) - .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3))) - .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3))) - .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v))) - .push( - Text::new(color.to_string()) - .width(Length::Units(185)) - .size(14), - ) - .into() + row![ + text(C::LABEL).width(Length::Units(50)), + slider(cr1, c1, move |v| C::new(v, c2, c3)), + slider(cr2, c2, move |v| C::new(c1, v, c3)), + slider(cr3, c3, move |v| C::new(c1, c2, v)), + text(color.to_string()).width(Length::Units(185)).size(14), + ] + .spacing(10) + .align_items(Alignment::Center) + .into() } } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 6a8f53e21a..06b1e53a41 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -1,5 +1,7 @@ -use iced::{Container, Element, Length, Sandbox, Settings}; -use numeric_input::NumericInput; +use iced::widget::container; +use iced::{Element, Length, Sandbox, Settings}; + +use numeric_input::numeric_input; pub fn main() -> iced::Result { Component::run(Settings::default()) @@ -7,7 +9,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Component { - numeric_input: numeric_input::State, value: Option, } @@ -35,39 +36,31 @@ impl Sandbox for Component { } } - fn view(&mut self) -> Element { - Container::new(NumericInput::new( - &mut self.numeric_input, - self.value, - Message::NumericInputChanged, - )) - .padding(20) - .height(Length::Fill) - .center_y() - .into() + fn view(&self) -> Element { + container(numeric_input(self.value, Message::NumericInputChanged)) + .padding(20) + .height(Length::Fill) + .center_y() + .into() } } mod numeric_input { - use iced_lazy::component::{self, Component}; - use iced_native::alignment::{self, Alignment}; - use iced_native::text; - use iced_native::widget::button::{self, Button}; - use iced_native::widget::text_input::{self, TextInput}; - use iced_native::widget::{self, Row, Text}; - use iced_native::{Element, Length}; - - pub struct NumericInput<'a, Message> { - state: &'a mut State, + use iced::alignment::{self, Alignment}; + use iced::widget::{self, button, row, text, text_input}; + use iced::{Element, Length}; + use iced_lazy::{self, Component}; + + pub struct NumericInput { value: Option, on_change: Box) -> Message>, } - #[derive(Default)] - pub struct State { - input: text_input::State, - decrement_button: button::State, - increment_button: button::State, + pub fn numeric_input( + value: Option, + on_change: impl Fn(Option) -> Message + 'static, + ) -> NumericInput { + NumericInput::new(value, on_change) } #[derive(Debug, Clone)] @@ -77,31 +70,33 @@ mod numeric_input { DecrementPressed, } - impl<'a, Message> NumericInput<'a, Message> { + impl NumericInput { pub fn new( - state: &'a mut State, value: Option, on_change: impl Fn(Option) -> Message + 'static, ) -> Self { Self { - state, value, on_change: Box::new(on_change), } } } - impl<'a, Message, Renderer> Component - for NumericInput<'a, Message> + impl Component for NumericInput where - Renderer: 'a + text::Renderer, - Renderer::Theme: button::StyleSheet - + text_input::StyleSheet + Renderer: iced_native::text::Renderer + 'static, + Renderer::Theme: widget::button::StyleSheet + + widget::text_input::StyleSheet + widget::text::StyleSheet, { + type State = (); type Event = Event; - fn update(&mut self, event: Event) -> Option { + 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), @@ -123,11 +118,10 @@ mod numeric_input { } } - fn view(&mut self) -> Element { - let button = |state, label, on_press| { - Button::new( - state, - Text::new(label) + fn view(&self, _state: &Self::State) -> Element { + let button = |label, on_press| { + button( + text(label) .width(Length::Fill) .height(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center) @@ -137,15 +131,9 @@ mod numeric_input { .on_press(on_press) }; - Row::with_children(vec![ - button( - &mut self.state.decrement_button, - "-", - Event::DecrementPressed, - ) - .into(), - TextInput::new( - &mut self.state.input, + row![ + button("-", Event::DecrementPressed), + text_input( "Type a number", self.value .as_ref() @@ -154,32 +142,26 @@ mod numeric_input { .unwrap_or(""), Event::InputChanged, ) - .padding(10) - .into(), - button( - &mut self.state.increment_button, - "+", - Event::IncrementPressed, - ) - .into(), - ]) + .padding(10), + button("+", Event::IncrementPressed), + ] .align_items(Alignment::Fill) .spacing(10) .into() } } - impl<'a, Message, Renderer> From> + impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: button::StyleSheet - + text_input::StyleSheet + Renderer: 'static + iced_native::text::Renderer, + Renderer::Theme: widget::button::StyleSheet + + widget::text_input::StyleSheet + widget::text::StyleSheet, { - fn from(numeric_input: NumericInput<'a, Message>) -> Self { - component::view(numeric_input) + fn from(numeric_input: NumericInput) -> Self { + iced_lazy::component(numeric_input) } } } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index e92f07f25d..13dcbf8615 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,15 +1,12 @@ -use iced::button::{self, Button}; -use iced::{Alignment, Column, Element, Sandbox, Settings, Text}; +use iced::widget::{button, column, text}; +use iced::{Alignment, Element, Sandbox, Settings}; pub fn main() -> iced::Result { Counter::run(Settings::default()) } -#[derive(Default)] struct Counter { value: i32, - increment_button: button::State, - decrement_button: button::State, } #[derive(Debug, Clone, Copy)] @@ -22,7 +19,7 @@ impl Sandbox for Counter { type Message = Message; fn new() -> Self { - Self::default() + Self { value: 0 } } fn title(&self) -> String { @@ -40,19 +37,14 @@ impl Sandbox for Counter { } } - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .align_items(Alignment::Center) - .push( - Button::new(&mut self.increment_button, Text::new("Increment")) - .on_press(Message::IncrementPressed), - ) - .push(Text::new(self.value.to_string()).size(50)) - .push( - Button::new(&mut self.decrement_button, Text::new("Decrement")) - .on_press(Message::DecrementPressed), - ) - .into() + fn view(&self) -> Element { + column![ + button("Increment").on_press(Message::IncrementPressed), + text(self.value).size(50), + button("Decrement").on_press(Message::DecrementPressed) + ] + .padding(20) + .align_items(Alignment::Center) + .into() } } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index d1a7bb065f..c37a1a1212 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -11,7 +11,8 @@ mod circle { // implemented by `iced_wgpu` and other renderers. use iced_native::layout::{self, Layout}; use iced_native::renderer; - use iced_native::{Color, Element, Length, Point, Rectangle, Size, Widget}; + use iced_native::widget::{self, Widget}; + use iced_native::{Color, Element, Length, Point, Rectangle, Size}; pub struct Circle { radius: f32, @@ -23,6 +24,10 @@ mod circle { } } + pub fn circle(radius: f32) -> Circle { + Circle::new(radius) + } + impl Widget for Circle where Renderer: renderer::Renderer, @@ -45,6 +50,7 @@ mod circle { fn draw( &self, + _state: &widget::Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, @@ -74,11 +80,9 @@ mod circle { } } -use circle::Circle; -use iced::{ - slider, Alignment, Column, Container, Element, Length, Sandbox, Settings, - Slider, Text, -}; +use circle::circle; +use iced::widget::{column, container, slider, text}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings::default()) @@ -86,7 +90,6 @@ pub fn main() -> iced::Result { struct Example { radius: f32, - slider: slider::State, } #[derive(Debug, Clone, Copy)] @@ -98,10 +101,7 @@ impl Sandbox for Example { type Message = Message; fn new() -> Self { - Example { - radius: 50.0, - slider: slider::State::new(), - } + Example { radius: 50.0 } } fn title(&self) -> String { @@ -116,25 +116,18 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Alignment::Center) - .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {:.2}", self.radius))) - .push( - Slider::new( - &mut self.slider, - 1.0..=100.0, - self.radius, - Message::RadiusChanged, - ) - .step(0.01), - ); - - Container::new(content) + fn view(&self) -> Element { + let content = column![ + circle(self.radius), + text(format!("Radius: {:.2}", self.radius)), + slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01), + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Alignment::Center); + + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 2999bc7e80..3ef9ef7a30 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,8 +1,8 @@ -use iced::button; use iced::executor; +use iced::widget::{button, column, container, progress_bar, text, Column}; use iced::{ - Alignment, Application, Button, Column, Command, Container, Element, - Length, ProgressBar, Settings, Subscription, Text, Theme, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; mod download; @@ -15,7 +15,6 @@ pub fn main() -> iced::Result { struct Example { downloads: Vec, last_id: usize, - add: button::State, } #[derive(Debug, Clone)] @@ -36,7 +35,6 @@ impl Application for Example { Example { downloads: vec![Download::new(0)], last_id: 0, - add: button::State::new(), }, Command::none(), ) @@ -74,21 +72,19 @@ impl Application for Example { Subscription::batch(self.downloads.iter().map(Download::subscription)) } - fn view(&mut self) -> Element { - let downloads = self - .downloads - .iter_mut() - .fold(Column::new().spacing(20), |column, download| { - column.push(download.view()) - }) - .push( - Button::new(&mut self.add, Text::new("Add another download")) - .on_press(Message::Add) - .padding(10), - ) - .align_items(Alignment::End); - - Container::new(downloads) + fn view(&self) -> Element { + let downloads = Column::with_children( + self.downloads.iter().map(Download::view).collect(), + ) + .push( + button("Add another download") + .on_press(Message::Add) + .padding(10), + ) + .spacing(20) + .align_items(Alignment::End); + + container(downloads) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -106,19 +102,17 @@ struct Download { #[derive(Debug)] enum State { - Idle { button: button::State }, + Idle, Downloading { progress: f32 }, - Finished { button: button::State }, - Errored { button: button::State }, + Finished, + Errored, } impl Download { pub fn new(id: usize) -> Self { Download { id, - state: State::Idle { - button: button::State::new(), - }, + state: State::Idle, } } @@ -143,14 +137,10 @@ impl Download { *progress = percentage; } download::Progress::Finished => { - self.state = State::Finished { - button: button::State::new(), - } + self.state = State::Finished; } download::Progress::Errored => { - self.state = State::Errored { - button: button::State::new(), - }; + self.state = State::Errored; } } } @@ -166,7 +156,7 @@ impl Download { } } - pub fn view(&mut self) -> Element { + pub fn view(&self) -> Element { let current_progress = match &self.state { State::Idle { .. } => 0.0, State::Downloading { progress } => *progress, @@ -174,36 +164,28 @@ impl Download { State::Errored { .. } => 0.0, }; - let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); + let progress_bar = progress_bar(0.0..=100.0, current_progress); - let control: Element<_> = match &mut self.state { - State::Idle { button } => { - Button::new(button, Text::new("Start the download!")) - .on_press(Message::Download(self.id)) + let control: Element<_> = match &self.state { + State::Idle => button("Start the download!") + .on_press(Message::Download(self.id)) + .into(), + State::Finished => { + column!["Download finished!", button("Start again")] + .spacing(10) + .align_items(Alignment::Center) .into() } - State::Finished { button } => Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Download finished!")) - .push( - Button::new(button, Text::new("Start again")) - .on_press(Message::Download(self.id)), - ) - .into(), State::Downloading { .. } => { - Text::new(format!("Downloading... {:.2}%", current_progress)) - .into() + text(format!("Downloading... {:.2}%", current_progress)).into() } - State::Errored { button } => Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Something went wrong :(")) - .push( - Button::new(button, Text::new("Try again")) - .on_press(Message::Download(self.id)), - ) - .into(), + State::Errored => column![ + "Something went wrong :(", + button("Try again").on_press(Message::Download(self.id)), + ] + .spacing(10) + .align_items(Alignment::Center) + .into(), }; Column::new() diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index c87fbc7258..234e14239d 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,9 +1,9 @@ use iced::alignment; -use iced::button; use iced::executor; +use iced::widget::{button, checkbox, container, text, Column}; use iced::{ - Alignment, Application, Button, Checkbox, Column, Command, Container, - Element, Length, Settings, Subscription, Text, Theme, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use iced_native::{window, Event}; @@ -18,7 +18,6 @@ pub fn main() -> iced::Result { struct Events { last: Vec, enabled: bool, - exit: button::State, should_exit: bool, } @@ -76,23 +75,23 @@ impl Application for Events { self.should_exit } - fn view(&mut self) -> Element { - let events = self.last.iter().fold( - Column::new().spacing(10), - |column, event| { - column.push(Text::new(format!("{:?}", event)).size(40)) - }, + fn view(&self) -> Element { + let events = Column::with_children( + self.last + .iter() + .map(|event| text(format!("{:?}", event)).size(40)) + .map(Element::from) + .collect(), ); - let toggle = Checkbox::new( - self.enabled, + let toggle = checkbox( "Listen to runtime events", + self.enabled, Message::Toggled, ); - let exit = Button::new( - &mut self.exit, - Text::new("Exit") + let exit = button( + text("Exit") .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) @@ -107,7 +106,7 @@ impl Application for Events { .push(toggle) .push(exit); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index c45a8205db..5d518d2fd0 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,7 +1,5 @@ -use iced::{ - button, Alignment, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, -}; +use iced::widget::{button, column, container}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Exit::run(Settings::default()) @@ -11,8 +9,6 @@ pub fn main() -> iced::Result { struct Exit { show_confirm: bool, exit: bool, - confirm_button: button::State, - exit_button: button::State, } #[derive(Debug, Clone, Copy)] @@ -47,33 +43,24 @@ impl Sandbox for Exit { } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let content = if self.show_confirm { - Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Are you sure you want to exit?")) - .push( - Button::new( - &mut self.confirm_button, - Text::new("Yes, exit now"), - ) + column![ + "Are you sure you want to exit?", + button("Yes, exit now") .padding([10, 20]) .on_press(Message::Confirm), - ) + ] } else { - Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Click the button to exit")) - .push( - Button::new(&mut self.exit_button, Text::new("Exit")) - .padding([10, 20]) - .on_press(Message::Exit), - ) - }; + column![ + "Click the button to exit", + button("Exit").padding([10, 20]).on_press(Message::Exit), + ] + } + .spacing(10) + .align_items(Alignment::Center); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 62ecc2d158..a2030275dd 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -3,18 +3,18 @@ mod preset; use grid::Grid; -use iced::button::{self, Button}; +use preset::Preset; + use iced::executor; -use iced::pick_list::{self, PickList}; -use iced::slider::{self, Slider}; use iced::theme::{self, Theme}; use iced::time; +use iced::widget::{ + button, checkbox, column, container, pick_list, row, slider, text, +}; use iced::window; use iced::{ - Alignment, Application, Checkbox, Column, Command, Element, Length, Row, - Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, }; -use preset::Preset; use std::time::{Duration, Instant}; pub fn main() -> iced::Result { @@ -33,7 +33,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct GameOfLife { grid: Grid, - controls: Controls, is_playing: bool, queued_ticks: usize, speed: usize, @@ -132,23 +131,26 @@ impl Application for GameOfLife { } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let version = self.version; let selected_speed = self.next_speed.unwrap_or(self.speed); - let controls = self.controls.view( + let controls = view_controls( self.is_playing, self.grid.are_lines_visible(), selected_speed, self.grid.preset(), ); - Column::new() - .push( - self.grid - .view() - .map(move |message| Message::Grid(message, version)), - ) - .push(controls) + let content = column![ + self.grid + .view() + .map(move |message| Message::Grid(message, version)), + controls + ]; + + container(content) + .width(Length::Fill) + .height(Length::Fill) .into() } @@ -157,13 +159,59 @@ impl Application for GameOfLife { } } +fn view_controls<'a>( + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + preset: Preset, +) -> Element<'a, Message> { + let playback_controls = row![ + button(if is_playing { "Pause" } else { "Play" }) + .on_press(Message::TogglePlayback), + button("Next") + .on_press(Message::Next) + .style(theme::Button::Secondary), + ] + .spacing(10); + + let speed_controls = row![ + slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), + text(format!("x{}", speed)).size(16), + ] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10); + + row![ + playback_controls, + speed_controls, + checkbox("Grid", is_grid_enabled, Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + pick_list(preset::ALL, Some(preset), Message::PresetPicked) + .padding(8) + .text_size(16), + button("Clear") + .on_press(Message::Clear) + .style(theme::Button::Destructive), + ] + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .into() +} + mod grid { use crate::Preset; + use iced::widget::canvas; + use iced::widget::canvas::event::{self, Event}; + use iced::widget::canvas::{ + Cache, Canvas, Cursor, Frame, Geometry, Path, Text, + }; use iced::{ - alignment, - canvas::event::{self, Event}, - canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text}, - mouse, Color, Element, Length, Point, Rectangle, Size, Theme, Vector, + alignment, mouse, Color, Element, Length, Point, Rectangle, Size, + Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -173,7 +221,6 @@ mod grid { pub struct Grid { state: State, preset: Preset, - interaction: Interaction, life_cache: Cache, grid_cache: Cache, translation: Vector, @@ -187,6 +234,8 @@ mod grid { pub enum Message { Populate(Cell), Unpopulate(Cell), + Translated(Vector), + Scaled(f32, Option), Ticked { result: Result, tick_duration: Duration, @@ -218,7 +267,6 @@ mod grid { .collect(), ), preset, - interaction: Interaction::None, life_cache: Cache::default(), grid_cache: Cache::default(), translation: Vector::default(), @@ -263,6 +311,22 @@ mod grid { 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, @@ -280,7 +344,7 @@ mod grid { } } - pub fn view(&mut self) -> Element { + pub fn view(&self) -> Element { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -329,14 +393,17 @@ mod grid { } impl canvas::Program for Grid { + type State = Interaction; + fn update( - &mut self, + &self, + interaction: &mut Interaction, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option) { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - self.interaction = Interaction::None; + *interaction = Interaction::None; } let cursor_position = @@ -360,7 +427,7 @@ mod grid { mouse::Event::ButtonPressed(button) => { let message = match button { mouse::Button::Left => { - self.interaction = if is_populated { + *interaction = if is_populated { Interaction::Erasing } else { Interaction::Drawing @@ -369,7 +436,7 @@ mod grid { populate.or(unpopulate) } mouse::Button::Right => { - self.interaction = Interaction::Panning { + *interaction = Interaction::Panning { translation: self.translation, start: cursor_position, }; @@ -382,23 +449,20 @@ mod grid { (event::Status::Captured, message) } mouse::Event::CursorMoved { .. } => { - let message = match self.interaction { + let message = match *interaction { Interaction::Drawing => populate, Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { - self.translation = translation - + (cursor_position - start) - * (1.0 / self.scaling); - - self.life_cache.clear(); - self.grid_cache.clear(); - - None + Some(Message::Translated( + translation + + (cursor_position - start) + * (1.0 / self.scaling), + )) } _ => None, }; - let event_status = match self.interaction { + let event_status = match interaction { Interaction::None => event::Status::Ignored, _ => event::Status::Captured, }; @@ -413,30 +477,38 @@ mod grid { { let old_scaling = self.scaling; - self.scaling = (self.scaling - * (1.0 + y / 30.0)) + let scaling = (self.scaling * (1.0 + y / 30.0)) .max(Self::MIN_SCALING) .min(Self::MAX_SCALING); - if let Some(cursor_to_center) = - cursor.position_from(bounds.center()) - { - let factor = self.scaling - old_scaling; - - self.translation = self.translation - - Vector::new( - cursor_to_center.x * factor - / (old_scaling * old_scaling), - cursor_to_center.y * factor - / (old_scaling * old_scaling), - ); - } - - self.life_cache.clear(); - self.grid_cache.clear(); + 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::Captured, None) } }, _ => (event::Status::Ignored, None), @@ -447,6 +519,7 @@ mod grid { fn draw( &self, + _interaction: &Interaction, _theme: &Theme, bounds: Rectangle, cursor: Cursor, @@ -576,10 +649,11 @@ mod grid { fn mouse_interaction( &self, + interaction: &Interaction, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { - match self.interaction { + match interaction { Interaction::Drawing => mouse::Interaction::Crosshair, Interaction::Erasing => mouse::Interaction::Crosshair, Interaction::Panning { .. } => mouse::Interaction::Grabbing, @@ -808,86 +882,16 @@ mod grid { } } - enum Interaction { + pub enum Interaction { None, Drawing, Erasing, Panning { translation: Vector, start: Point }, } -} - -#[derive(Default)] -struct Controls { - toggle_button: button::State, - next_button: button::State, - clear_button: button::State, - speed_slider: slider::State, - preset_list: pick_list::State, -} -impl Controls { - fn view( - &mut self, - is_playing: bool, - is_grid_enabled: bool, - speed: usize, - preset: Preset, - ) -> Element { - let playback_controls = Row::new() - .spacing(10) - .push( - Button::new( - &mut self.toggle_button, - Text::new(if is_playing { "Pause" } else { "Play" }), - ) - .on_press(Message::TogglePlayback) - .style(theme::Button::Primary), - ) - .push( - Button::new(&mut self.next_button, Text::new("Next")) - .on_press(Message::Next) - .style(theme::Button::Secondary), - ); - - let speed_controls = Row::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(Slider::new( - &mut self.speed_slider, - 1.0..=1000.0, - speed as f32, - Message::SpeedChanged, - )) - .push(Text::new(format!("x{}", speed)).size(16)); - - Row::new() - .padding(10) - .spacing(20) - .align_items(Alignment::Center) - .push(playback_controls) - .push(speed_controls) - .push( - Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) - .size(16) - .spacing(5) - .text_size(16), - ) - .push( - PickList::new( - &mut self.preset_list, - preset::ALL, - Some(preset), - Message::PresetPicked, - ) - .padding(8) - .text_size(16), - ) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(theme::Button::Destructive), - ) - .into() + impl Default for Interaction { + fn default() -> Self { + Self::None + } } } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 03eac69ebc..d8b99ab31e 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -13,8 +13,9 @@ mod rainbow { use iced_graphics::renderer::{self, Renderer}; use iced_graphics::{Backend, Primitive}; + use iced_native::widget::{self, Widget}; use iced_native::{ - layout, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, + layout, Element, Layout, Length, Point, Rectangle, Size, Vector, }; #[derive(Default)] @@ -26,6 +27,10 @@ mod rainbow { } } + pub fn rainbow() -> Rainbow { + Rainbow + } + impl Widget> for Rainbow where B: Backend, @@ -50,6 +55,7 @@ mod rainbow { fn draw( &self, + _tree: &widget::Tree, renderer: &mut Renderer, _theme: &T, _style: &renderer::Style, @@ -159,27 +165,21 @@ mod rainbow { } } -use iced::{ - scrollable, Alignment, Column, Container, Element, Length, Sandbox, - Scrollable, Settings, Text, -}; -use rainbow::Rainbow; +use iced::widget::{column, container, scrollable}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; +use rainbow::rainbow; pub fn main() -> iced::Result { Example::run(Settings::default()) } -struct Example { - scroll: scrollable::State, -} +struct Example; impl Sandbox for Example { type Message = (); fn new() -> Self { - Example { - scroll: scrollable::State::new(), - } + Example } fn title(&self) -> String { @@ -188,32 +188,27 @@ impl Sandbox for Example { fn update(&mut self, _: ()) {} - fn view(&mut self) -> Element<()> { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Alignment::Start) - .push(Rainbow::new()) - .push(Text::new( - "In this example we draw a custom widget Rainbow, using \ + fn view(&self) -> Element<()> { + let content = column![ + rainbow(), + "In this example we draw a custom widget Rainbow, using \ the Mesh2D primitive. This primitive supplies a list of \ triangles, expressed as vertices and indices.", - )) - .push(Text::new( - "Move your cursor over it, and see the center vertex \ + "Move your cursor over it, and see the center vertex \ follow you!", - )) - .push(Text::new( - "Every Vertex2D defines its own color. You could use the \ + "Every Vertex2D defines its own color. You could use the \ Mesh2D primitive to render virtually any two-dimensional \ geometry for your widget.", - )); + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Alignment::Start); - let scrollable = Scrollable::new(&mut self.scroll) - .push(Container::new(content).width(Length::Fill).center_x()); + let scrollable = + scrollable(container(content).width(Length::Fill).center_x()); - Container::new(scrollable) + container(scrollable) .width(Length::Fill) .height(Length::Fill) .center_y() diff --git a/examples/integration_opengl/src/controls.rs b/examples/integration_opengl/src/controls.rs index fdaa29d57e..076d37d3f6 100644 --- a/examples/integration_opengl/src/controls.rs +++ b/examples/integration_opengl/src/controls.rs @@ -1,11 +1,10 @@ use iced_glow::Renderer; -use iced_glutin::widget::slider::{self, Slider}; +use iced_glutin::widget::Slider; use iced_glutin::widget::{Column, Row, Text}; use iced_glutin::{Alignment, Color, Command, Element, Length, Program}; pub struct Controls { background_color: Color, - sliders: [slider::State; 3], } #[derive(Debug, Clone)] @@ -17,7 +16,6 @@ impl Controls { pub fn new() -> Controls { Controls { background_color: Color::BLACK, - sliders: Default::default(), } } @@ -40,15 +38,14 @@ impl Program for Controls { Command::none() } - fn view(&mut self) -> Element { - let [r, g, b] = &mut self.sliders; + fn view(&self) -> Element { let background_color = self.background_color; let sliders = Row::new() .width(Length::Units(500)) .spacing(20) .push( - Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + Slider::new(0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color @@ -57,7 +54,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + Slider::new(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color @@ -66,7 +63,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(b, 0.0..=1.0, background_color.b, move |b| { + Slider::new(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color diff --git a/examples/integration_wgpu/src/controls.rs b/examples/integration_wgpu/src/controls.rs index 2f1daa91dd..6c41738cb5 100644 --- a/examples/integration_wgpu/src/controls.rs +++ b/examples/integration_wgpu/src/controls.rs @@ -1,14 +1,10 @@ use iced_wgpu::Renderer; -use iced_winit::widget::slider::{self, Slider}; -use iced_winit::widget::text_input::{self, TextInput}; -use iced_winit::widget::{Column, Row, Text}; +use iced_winit::widget::{slider, text_input, Column, Row, Text}; use iced_winit::{Alignment, Color, Command, Element, Length, Program}; pub struct Controls { background_color: Color, text: String, - sliders: [slider::State; 3], - text_input: text_input::State, } #[derive(Debug, Clone)] @@ -22,8 +18,6 @@ impl Controls { Controls { background_color: Color::BLACK, text: Default::default(), - sliders: Default::default(), - text_input: Default::default(), } } @@ -49,9 +43,7 @@ impl Program for Controls { Command::none() } - fn view(&mut self) -> Element { - let [r, g, b] = &mut self.sliders; - let t = &mut self.text_input; + fn view(&self) -> Element { let background_color = self.background_color; let text = &self.text; @@ -59,7 +51,7 @@ impl Program for Controls { .width(Length::Units(500)) .spacing(20) .push( - Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + slider(0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color @@ -68,7 +60,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + slider(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color @@ -77,7 +69,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(b, 0.0..=1.0, background_color.b, move |b| { + slider(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color @@ -108,8 +100,7 @@ impl Program for Controls { .size(14) .style(Color::WHITE), ) - .push(TextInput::new( - t, + .push(text_input( "Placeholder", text, Message::TextChanged, diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 5fbcea2c4e..ae8fa22b0c 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,15 +1,13 @@ use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; use iced::executor; use iced::keyboard; -use iced::pane_grid::{self, PaneGrid}; -use iced::scrollable::{self, Scrollable}; use iced::theme::{self, Theme}; +use iced::widget::pane_grid::{self, PaneGrid}; +use iced::widget::{button, column, container, row, scrollable, text}; use iced::{ - Application, Color, Column, Command, Container, Element, Length, Row, - Settings, Size, Subscription, Text, + Application, Color, Command, Element, Length, Settings, Size, Subscription, }; -use iced_lazy::responsive::{self, Responsive}; +use iced_lazy::responsive; use iced_native::{event, subscription, Event}; pub fn main() -> iced::Result { @@ -155,42 +153,32 @@ impl Application for Example { }) } - fn view(&mut self) -> Element { + 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 pane_grid = PaneGrid::new(&self.panes, |id, pane| { let is_focused = focus == Some(id); - let Pane { - responsive, + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(Message::TogglePin(id)) + .padding(3); + + let title = row![ pin_button, - is_pinned, - content, - .. - } = pane; - - let text = if *is_pinned { "Unpin" } else { "Pin" }; - let pin_button = Button::new(pin_button, Text::new(text).size(14)) - .on_press(Message::TogglePin(id)) - .style(theme::Button::Secondary) - .padding(3); - - let title = Row::with_children(vec![ - pin_button.into(), - Text::new("Pane").into(), - Text::new(content.id.to_string()) - .style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }) - .into(), - ]) + "Pane", + text(pane.id.to_string()).style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + ] .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(pane.controls.view(id, total_panes, *is_pinned)) + .controls(view_controls(id, total_panes, pane.is_pinned)) .padding(10) .style(if is_focused { style::title_bar_focused @@ -198,8 +186,8 @@ impl Application for Example { style::title_bar_active }); - pane_grid::Content::new(Responsive::new(responsive, move |size| { - content.view(id, total_panes, *is_pinned, size) + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, pane.is_pinned, size) })) .title_bar(title_bar) .style(if is_focused { @@ -215,7 +203,7 @@ impl Application for Example { .on_drag(Message::Dragged) .on_resize(10, Message::Resized); - Container::new(pane_grid) + container(pane_grid) .width(Length::Fill) .height(Length::Fill) .padding(10) @@ -255,139 +243,92 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { } struct Pane { - pub responsive: responsive::State, - pub is_pinned: bool, - pub pin_button: button::State, - pub content: Content, - pub controls: Controls, -} - -struct Content { id: usize, - scroll: scrollable::State, - split_horizontally: button::State, - split_vertically: button::State, - close: button::State, -} - -struct Controls { - close: button::State, + pub is_pinned: bool, } impl Pane { fn new(id: usize) -> Self { Self { - responsive: responsive::State::new(), + id, is_pinned: false, - pin_button: button::State::new(), - content: Content::new(id), - controls: Controls::new(), } } } -impl Content { - fn new(id: usize) -> Self { - Content { - id, - scroll: scrollable::State::new(), - split_horizontally: button::State::new(), - split_vertically: button::State::new(), - close: button::State::new(), - } +fn view_content<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, Message> { + let button = |label, message| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + }; + + let mut controls = column![ + button( + "Split horizontally", + Message::Split(pane_grid::Axis::Horizontal, pane), + ), + button( + "Split vertically", + Message::Split(pane_grid::Axis::Vertical, pane), + ) + ] + .spacing(5) + .max_width(150); + + if total_panes > 1 && !is_pinned { + controls = controls.push( + button("Close", Message::Close(pane)) + .style(theme::Button::Destructive), + ); } - fn view( - &mut self, - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - size: Size, - ) -> Element { - let Content { - scroll, - split_horizontally, - split_vertically, - close, - .. - } = self; - - let button = |state, label, message| { - Button::new( - state, - Text::new(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - }; - - let mut controls = Column::new() - .spacing(5) - .max_width(150) - .push(button( - split_horizontally, - "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), - )) - .push(button( - split_vertically, - "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), - )); - - if total_panes > 1 && !is_pinned { - controls = controls.push( - button(close, "Close", Message::Close(pane)) - .style(theme::Button::Destructive), - ); - } - let content = Scrollable::new(scroll) - .width(Length::Fill) - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new(format!("{}x{}", size.width, size.height)).size(24)) - .push(controls); + let content = column![ + text(format!("{}x{}", size.width, size.height)).size(24), + controls, + ] + .width(Length::Fill) + .spacing(10) + .align_items(Alignment::Center); - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() - } + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() } -impl Controls { - fn new() -> Self { - Self { - close: button::State::new(), - } +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(theme::Button::Destructive) + .padding(3); + + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); } - pub fn view( - &mut self, - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - ) -> Element { - let mut button = - Button::new(&mut self.close, Text::new("Close").size(14)) - .style(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); - } - button.into() - } + button.into() } mod style { - use iced::{container, Theme}; + use iced::widget::container; + use iced::Theme; pub fn title_bar_active(theme: &Theme) -> container::Appearance { let palette = theme.extended_palette(); diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 52303d705f..9df1f5c721 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,7 +1,5 @@ -use iced::{ - pick_list, scrollable, Alignment, Container, Element, Length, PickList, - Sandbox, Scrollable, Settings, Space, Text, -}; +use iced::widget::{column, container, pick_list, scrollable, vertical_space}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings::default()) @@ -9,8 +7,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Example { - scroll: scrollable::State, - pick_list: pick_list::State, selected_language: Option, } @@ -38,26 +34,25 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element { - let pick_list = PickList::new( - &mut self.pick_list, + fn view(&self) -> Element { + let pick_list = pick_list( &Language::ALL[..], self.selected_language, Message::LanguageSelected, ) .placeholder("Choose a language..."); - let mut content = Scrollable::new(&mut self.scroll) - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(Space::with_height(Length::Units(600))) - .push(Text::new("Which is your favorite language?")) - .push(pick_list); - - content = content.push(Space::with_height(Length::Units(600))); + let content = column![ + vertical_space(Length::Units(600)), + "Which is your favorite language?", + pick_list, + vertical_space(Length::Units(600)), + ] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10); - Container::new(content) + container(scrollable(content)) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index a1cf68e82c..4fe2d07cc2 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,9 +1,7 @@ -use iced::button; use iced::futures; -use iced::image; +use iced::widget::{self, column, container, image, row, text}; use iced::{ - Alignment, Application, Button, Color, Column, Command, Container, Element, - Length, Row, Settings, Text, Theme, + Alignment, Application, Color, Command, Element, Length, Settings, Theme, }; pub fn main() -> iced::Result { @@ -13,13 +11,8 @@ pub fn main() -> iced::Result { #[derive(Debug)] enum Pokedex { Loading, - Loaded { - pokemon: Pokemon, - search: button::State, - }, - Errored { - try_again: button::State, - }, + Loaded { pokemon: Pokemon }, + Errored, } #[derive(Debug, Clone)] @@ -54,17 +47,12 @@ impl Application for Pokedex { fn update(&mut self, message: Message) -> Command { match message { Message::PokemonFound(Ok(pokemon)) => { - *self = Pokedex::Loaded { - pokemon, - search: button::State::new(), - }; + *self = Pokedex::Loaded { pokemon }; Command::none() } Message::PokemonFound(Err(_error)) => { - *self = Pokedex::Errored { - try_again: button::State::new(), - }; + *self = Pokedex::Errored; Command::none() } @@ -79,27 +67,28 @@ impl Application for Pokedex { } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let content = match self { - Pokedex::Loading => Column::new() - .width(Length::Shrink) - .push(Text::new("Searching for Pokémon...").size(40)), - Pokedex::Loaded { pokemon, search } => Column::new() - .max_width(500) - .spacing(20) - .align_items(Alignment::End) - .push(pokemon.view()) - .push( - button(search, "Keep searching!").on_press(Message::Search), - ), - Pokedex::Errored { try_again, .. } => Column::new() - .spacing(20) - .align_items(Alignment::End) - .push(Text::new("Whoops! Something went wrong...").size(40)) - .push(button(try_again, "Try again").on_press(Message::Search)), + Pokedex::Loading => { + column![text("Searching for Pokémon...").size(40),] + .width(Length::Shrink) + } + Pokedex::Loaded { pokemon } => column![ + pokemon.view(), + button("Keep searching!").on_press(Message::Search) + ] + .max_width(500) + .spacing(20) + .align_items(Alignment::End), + Pokedex::Errored => column![ + text("Whoops! Something went wrong...").size(40), + button("Try again").on_press(Message::Search) + ] + .spacing(20) + .align_items(Alignment::End), }; - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -114,41 +103,30 @@ struct Pokemon { name: String, description: String, image: image::Handle, - image_viewer: image::viewer::State, } impl Pokemon { const TOTAL: u16 = 807; - fn view(&mut self) -> Element { - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(image::Viewer::new( - &mut self.image_viewer, - self.image.clone(), - )) - .push( - Column::new() - .spacing(20) - .push( - Row::new() - .align_items(Alignment::Center) - .spacing(20) - .push( - Text::new(&self.name) - .size(30) - .width(Length::Fill), - ) - .push( - Text::new(format!("#{}", self.number)) - .size(20) - .style(Color::from([0.5, 0.5, 0.5])), - ), - ) - .push(Text::new(&self.description)), - ) - .into() + fn view(&self) -> Element { + row![ + image::viewer(self.image.clone()), + column![ + row![ + text(&self.name).size(30).width(Length::Fill), + text(format!("#{}", self.number)) + .size(20) + .style(Color::from([0.5, 0.5, 0.5])), + ] + .align_items(Alignment::Center) + .spacing(20), + self.description.as_ref(), + ] + .spacing(20), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } async fn search() -> Result { @@ -204,7 +182,6 @@ impl Pokemon { .map(|c| if c.is_control() { ' ' } else { c }) .collect(), image, - image_viewer: image::viewer::State::new(), }) } @@ -240,6 +217,6 @@ impl From for Error { } } -fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { - Button::new(state, Text::new(text)).padding(10) +fn button(text: &str) -> widget::Button<'_, Message> { + widget::button(text).padding(10) } diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index c9a8e7986c..d4ebe4d31e 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -1,4 +1,5 @@ -use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; +use iced::widget::{column, progress_bar, slider}; +use iced::{Element, Sandbox, Settings}; pub fn main() -> iced::Result { Progress::run(Settings::default()) @@ -7,7 +8,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Progress { value: f32, - progress_bar_slider: slider::State, } #[derive(Debug, Clone, Copy)] @@ -32,19 +32,12 @@ impl Sandbox for Progress { } } - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .push(ProgressBar::new(0.0..=100.0, self.value)) - .push( - Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - ) - .step(0.01), - ) - .into() + fn view(&self) -> Element { + column![ + progress_bar(0.0..=100.0, self.value), + slider(0.0..=100.0, self.value, Message::SliderChanged).step(0.01) + ] + .padding(20) + .into() } } diff --git a/examples/pure/color_palette/Cargo.toml b/examples/pure/color_palette/Cargo.toml deleted file mode 100644 index d08309d59a..0000000000 --- a/examples/pure/color_palette/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "pure_color_palette" -version = "0.1.0" -authors = ["Clark Moody "] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure", "canvas", "palette"] } -palette = "0.6.0" diff --git a/examples/pure/color_palette/README.md b/examples/pure/color_palette/README.md deleted file mode 100644 index f90020b1d6..0000000000 --- a/examples/pure/color_palette/README.md +++ /dev/null @@ -1,15 +0,0 @@ -## Color palette - -A color palette generator, based on a user-defined root color. - - - -You can run it with `cargo run`: - -``` -cargo run --package pure_color_palette -``` diff --git a/examples/pure/color_palette/screenshot.png b/examples/pure/color_palette/screenshot.png deleted file mode 100644 index e8da35c4d5..0000000000 Binary files a/examples/pure/color_palette/screenshot.png and /dev/null differ diff --git a/examples/pure/color_palette/src/main.rs b/examples/pure/color_palette/src/main.rs deleted file mode 100644 index 8a58afa764..0000000000 --- a/examples/pure/color_palette/src/main.rs +++ /dev/null @@ -1,465 +0,0 @@ -use iced::pure::{ - column, row, text, - widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}, - widget::Slider, - Element, Sandbox, -}; -use iced::{ - alignment, Alignment, Color, Length, Point, Rectangle, Settings, Size, - Vector, -}; -use palette::{self, convert::FromColor, Hsl, Srgb}; -use std::marker::PhantomData; -use std::ops::RangeInclusive; - -pub fn main() -> iced::Result { - ColorPalette::run(Settings { - antialiasing: true, - ..Settings::default() - }) -} - -#[derive(Default)] -pub struct ColorPalette { - theme: Theme, - rgb: ColorPicker, - hsl: ColorPicker, - hsv: ColorPicker, - hwb: ColorPicker, - lab: ColorPicker, - lch: ColorPicker, -} - -#[derive(Debug, Clone, Copy)] -pub enum Message { - RgbColorChanged(Color), - HslColorChanged(palette::Hsl), - HsvColorChanged(palette::Hsv), - HwbColorChanged(palette::Hwb), - LabColorChanged(palette::Lab), - LchColorChanged(palette::Lch), -} - -impl Sandbox for ColorPalette { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Color palette - Iced") - } - - fn update(&mut self, message: Message) { - let srgb = match message { - Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), - Message::HslColorChanged(hsl) => palette::Srgb::from_color(hsl), - Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv), - Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb), - Message::LabColorChanged(lab) => palette::Srgb::from_color(lab), - Message::LchColorChanged(lch) => palette::Srgb::from_color(lch), - }; - - self.theme = Theme::new(srgb); - } - - fn view(&self) -> Element { - let base = self.theme.base; - - let srgb = palette::Srgb::from(base); - let hsl = palette::Hsl::from_color(srgb); - let hsv = palette::Hsv::from_color(srgb); - let hwb = palette::Hwb::from_color(srgb); - let lab = palette::Lab::from_color(srgb); - let lch = palette::Lch::from_color(srgb); - - column() - .padding(10) - .spacing(10) - .push(self.rgb.view(base).map(Message::RgbColorChanged)) - .push(self.hsl.view(hsl).map(Message::HslColorChanged)) - .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) - .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) - .push(self.lab.view(lab).map(Message::LabColorChanged)) - .push(self.lch.view(lch).map(Message::LchColorChanged)) - .push(self.theme.view()) - .into() - } -} - -#[derive(Debug)] -struct Theme { - lower: Vec, - base: Color, - higher: Vec, - canvas_cache: canvas::Cache, -} - -impl Theme { - pub fn new(base: impl Into) -> Theme { - use palette::{Hue, Shade}; - - let base = base.into(); - - // Convert to HSL color for manipulation - let hsl = Hsl::from_color(Srgb::from(base)); - - let lower = [ - hsl.shift_hue(-135.0).lighten(0.075), - hsl.shift_hue(-120.0), - hsl.shift_hue(-105.0).darken(0.075), - hsl.darken(0.075), - ]; - - let higher = [ - hsl.lighten(0.075), - hsl.shift_hue(105.0).darken(0.075), - hsl.shift_hue(120.0), - hsl.shift_hue(135.0).lighten(0.075), - ]; - - Theme { - lower: lower - .iter() - .map(|&color| Srgb::from_color(color).into()) - .collect(), - base, - higher: higher - .iter() - .map(|&color| Srgb::from_color(color).into()) - .collect(), - canvas_cache: canvas::Cache::default(), - } - } - - pub fn len(&self) -> usize { - self.lower.len() + self.higher.len() + 1 - } - - pub fn colors(&self) -> impl Iterator { - self.lower - .iter() - .chain(std::iter::once(&self.base)) - .chain(self.higher.iter()) - } - - pub fn view(&self) -> Element { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - - fn draw(&self, frame: &mut Frame) { - let pad = 20.0; - - let box_size = Size { - width: frame.width() / self.len() as f32, - height: frame.height() / 2.0 - pad, - }; - - let triangle = Path::new(|path| { - path.move_to(Point { x: 0.0, y: -0.5 }); - path.line_to(Point { x: -0.5, y: 0.0 }); - path.line_to(Point { x: 0.5, y: 0.0 }); - path.close(); - }); - - let mut text = canvas::Text { - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Top, - size: 15.0, - ..canvas::Text::default() - }; - - for (i, &color) in self.colors().enumerate() { - let anchor = Point { - x: (i as f32) * box_size.width, - y: 0.0, - }; - frame.fill_rectangle(anchor, box_size, color); - - // We show a little indicator for the base color - if color == self.base { - let triangle_x = anchor.x + box_size.width / 2.0; - - frame.with_save(|frame| { - frame.translate(Vector::new(triangle_x, 0.0)); - frame.scale(10.0); - frame.rotate(std::f32::consts::PI); - - frame.fill(&triangle, Color::WHITE); - }); - - frame.with_save(|frame| { - frame.translate(Vector::new(triangle_x, box_size.height)); - frame.scale(10.0); - - frame.fill(&triangle, Color::WHITE); - }); - } - - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height, - }, - ..text - }); - } - - text.vertical_alignment = alignment::Vertical::Bottom; - - let hsl = Hsl::from_color(Srgb::from(self.base)); - for i in 0..self.len() { - let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); - let graded = Hsl { - lightness: 1.0 - pct, - ..hsl - }; - let color: Color = Srgb::from_color(graded).into(); - - let anchor = Point { - x: (i as f32) * box_size.width, - y: box_size.height + 2.0 * pad, - }; - - frame.fill_rectangle(anchor, box_size, color); - - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height + 2.0 * pad, - }, - ..text - }); - } - } -} - -impl canvas::Program for Theme { - type State = (); - - fn draw( - &self, - _state: &Self::State, - _theme: &iced::Theme, - bounds: Rectangle, - _cursor: Cursor, - ) -> Vec { - let theme = self.canvas_cache.draw(bounds.size(), |frame| { - self.draw(frame); - }); - - vec![theme] - } -} - -impl Default for Theme { - fn default() -> Self { - Theme::new(Color::from_rgb8(75, 128, 190)) - } -} - -fn color_hex_string(color: &Color) -> String { - format!( - "#{:x}{:x}{:x}", - (255.0 * color.r).round() as u8, - (255.0 * color.g).round() as u8, - (255.0 * color.b).round() as u8 - ) -} - -#[derive(Default)] -struct ColorPicker { - color_space: PhantomData, -} - -trait ColorSpace: Sized { - const LABEL: &'static str; - const COMPONENT_RANGES: [RangeInclusive; 3]; - - fn new(a: f32, b: f32, c: f32) -> Self; - - fn components(&self) -> [f32; 3]; - - fn to_string(&self) -> String; -} - -impl ColorPicker { - fn view(&self, color: C) -> Element { - let [c1, c2, c3] = color.components(); - let [cr1, cr2, cr3] = C::COMPONENT_RANGES; - - fn slider<'a, C: Clone>( - range: RangeInclusive, - component: f32, - update: impl Fn(f32) -> C + 'a, - ) -> Slider<'a, f64, C, iced::Renderer> { - Slider::new(range, f64::from(component), move |v| update(v as f32)) - .step(0.01) - } - - row() - .spacing(10) - .align_items(Alignment::Center) - .push(text(C::LABEL).width(Length::Units(50))) - .push(slider(cr1, c1, move |v| C::new(v, c2, c3))) - .push(slider(cr2, c2, move |v| C::new(c1, v, c3))) - .push(slider(cr3, c3, move |v| C::new(c1, c2, v))) - .push(text(color.to_string()).width(Length::Units(185)).size(14)) - .into() - } -} - -impl ColorSpace for Color { - const LABEL: &'static str = "RGB"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(r: f32, g: f32, b: f32) -> Self { - Color::from_rgb(r, g, b) - } - - fn components(&self) -> [f32; 3] { - [self.r, self.g, self.b] - } - - fn to_string(&self) -> String { - format!( - "rgb({:.0}, {:.0}, {:.0})", - 255.0 * self.r, - 255.0 * self.g, - 255.0 * self.b - ) - } -} - -impl ColorSpace for palette::Hsl { - const LABEL: &'static str = "HSL"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, saturation: f32, lightness: f32) -> Self { - palette::Hsl::new( - palette::RgbHue::from_degrees(hue), - saturation, - lightness, - ) - } - - fn components(&self) -> [f32; 3] { - [ - self.hue.to_positive_degrees(), - self.saturation, - self.lightness, - ] - } - - fn to_string(&self) -> String { - format!( - "hsl({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.saturation, - 100.0 * self.lightness - ) - } -} - -impl ColorSpace for palette::Hsv { - const LABEL: &'static str = "HSV"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, saturation: f32, value: f32) -> Self { - palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value) - } - - fn components(&self) -> [f32; 3] { - [self.hue.to_positive_degrees(), self.saturation, self.value] - } - - fn to_string(&self) -> String { - format!( - "hsv({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.saturation, - 100.0 * self.value - ) - } -} - -impl ColorSpace for palette::Hwb { - const LABEL: &'static str = "HWB"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { - palette::Hwb::new( - palette::RgbHue::from_degrees(hue), - whiteness, - blackness, - ) - } - - fn components(&self) -> [f32; 3] { - [ - self.hue.to_positive_degrees(), - self.whiteness, - self.blackness, - ] - } - - fn to_string(&self) -> String { - format!( - "hwb({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.whiteness, - 100.0 * self.blackness - ) - } -} - -impl ColorSpace for palette::Lab { - const LABEL: &'static str = "Lab"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; - - fn new(l: f32, a: f32, b: f32) -> Self { - palette::Lab::new(l, a, b) - } - - fn components(&self) -> [f32; 3] { - [self.l, self.a, self.b] - } - - fn to_string(&self) -> String { - format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b) - } -} - -impl ColorSpace for palette::Lch { - const LABEL: &'static str = "Lch"; - const COMPONENT_RANGES: [RangeInclusive; 3] = - [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; - - fn new(l: f32, chroma: f32, hue: f32) -> Self { - palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue)) - } - - fn components(&self) -> [f32; 3] { - [self.l, self.chroma, self.hue.to_positive_degrees()] - } - - fn to_string(&self) -> String { - format!( - "Lch({:.1}, {:.1}, {:.1})", - self.l, - self.chroma, - self.hue.to_positive_degrees() - ) - } -} diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml deleted file mode 100644 index b6c7a5136a..0000000000 --- a/examples/pure/component/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[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"] } -iced_pure = { path = "../../../pure" } diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs deleted file mode 100644 index db22d019fb..0000000000 --- a/examples/pure/component/src/main.rs +++ /dev/null @@ -1,172 +0,0 @@ -use iced::pure::container; -use iced::pure::{Element, Sandbox}; -use iced::{Length, Settings}; - -use numeric_input::numeric_input; - -pub fn main() -> iced::Result { - Component::run(Settings::default()) -} - -#[derive(Default)] -struct Component { - value: Option, -} - -#[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_lazy::pure::{self, Component}; - use iced_native::alignment::{self, Alignment}; - use iced_native::text; - use iced_native::widget; - use iced_native::Length; - use iced_pure::Element; - use iced_pure::{button, row, text, text_input}; - - 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, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { - 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| { - 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_deref() - .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, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { - fn from(numeric_input: NumericInput) -> Self { - pure::component(numeric_input) - } - } -} diff --git a/examples/pure/counter/Cargo.toml b/examples/pure/counter/Cargo.toml deleted file mode 100644 index 2fcd22d4fd..0000000000 --- a/examples/pure/counter/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "pure_counter" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs deleted file mode 100644 index 726009dfe2..0000000000 --- a/examples/pure/counter/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use iced::pure::{button, column, text, Element, Sandbox}; -use iced::{Alignment, Settings}; - -pub fn main() -> iced::Result { - Counter::run(Settings::default()) -} - -struct Counter { - value: i32, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - IncrementPressed, - DecrementPressed, -} - -impl Sandbox for Counter { - type Message = Message; - - fn new() -> Self { - Self { value: 0 } - } - - fn title(&self) -> String { - String::from("Counter - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::IncrementPressed => { - self.value += 1; - } - Message::DecrementPressed => { - self.value -= 1; - } - } - } - - fn view(&self) -> Element { - column() - .padding(20) - .align_items(Alignment::Center) - .push(button("Increment").on_press(Message::IncrementPressed)) - .push(text(self.value.to_string()).size(50)) - .push(button("Decrement").on_press(Message::DecrementPressed)) - .into() - } -} diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml deleted file mode 100644 index 22e38f00c8..0000000000 --- a/examples/pure/game_of_life/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[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 deleted file mode 100644 index aa39201c86..0000000000 --- a/examples/pure/game_of_life/README.md +++ /dev/null @@ -1,22 +0,0 @@ -## 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 deleted file mode 100644 index cf6560d644..0000000000 --- a/examples/pure/game_of_life/src/main.rs +++ /dev/null @@ -1,903 +0,0 @@ -//! 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; - -use grid::Grid; -use preset::Preset; - -use iced::executor; -use iced::pure::{ - button, checkbox, column, container, pick_list, row, slider, text, -}; -use iced::pure::{Application, Element}; -use iced::theme::{self, Theme}; -use iced::time; -use iced::window; -use iced::{Alignment, Command, Length, Settings, Subscription}; -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 Theme = Theme; - 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 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) - .into() - } - - fn theme(&self) -> Theme { - Theme::Dark - } -} - -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), - ) - .push( - button("Next") - .on_press(Message::Next) - .style(theme::Button::Secondary), - ); - - let speed_controls = row() - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(slider(1.0..=1000.0, speed as f32, Message::SpeedChanged)) - .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), - ) - .push( - button("Clear") - .on_press(Message::Clear) - .style(theme::Button::Destructive), - ) - .into() -} - -mod grid { - use crate::Preset; - use iced::pure::widget::canvas::event::{self, Event}; - use iced::pure::widget::canvas::{ - self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text, - }; - use iced::pure::Element; - use iced::{ - alignment, mouse, Color, Length, Point, Rectangle, Size, Theme, 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(&self) -> Element { - 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 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, - _theme: &Theme, - 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 deleted file mode 100644 index 964b912036..0000000000 --- a/examples/pure/game_of_life/src/preset.rs +++ /dev/null @@ -1,142 +0,0 @@ -#[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/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml deleted file mode 100644 index a51cdaf0bb..0000000000 --- a/examples/pure/pane_grid/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[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 deleted file mode 100644 index e85ed78dcf..0000000000 --- a/examples/pure/pane_grid/src/main.rs +++ /dev/null @@ -1,369 +0,0 @@ -use iced::alignment::{self, Alignment}; -use iced::executor; -use iced::keyboard; -use iced::pure::widget::pane_grid::{self, PaneGrid}; -use iced::pure::{button, column, container, row, scrollable, text}; -use iced::pure::{Application, Element}; -use iced::theme::{self, Theme}; -use iced::{Color, Command, Length, Settings, Size, Subscription}; -use iced_lazy::pure::responsive; -use iced_native::{event, subscription, Event}; - -pub fn main() -> iced::Result { - Example::run(Settings::default()) -} - -struct Example { - panes: pane_grid::State, - 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 Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - let (panes, _) = pane_grid::State::new(Pane::new(0)); - - ( - Example { - panes, - panes_created: 1, - focus: None, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Pane grid - Iced") - } - - fn update(&mut self, message: Message) -> Command { - 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(&self.panes, |id, pane| { - let is_focused = focus == Some(id); - - let pin_button = button( - text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), - ) - .on_press(Message::TogglePin(id)) - .padding(3); - - let title = row() - .push(pin_button) - .push("Pane") - .push(text(pane.id.to_string()).style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - })) - .spacing(5); - - let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls(id, total_panes, pane.is_pinned)) - .padding(10) - .style(if is_focused { - style::title_bar_focused - } else { - style::title_bar_active - }); - - pane_grid::Content::new(responsive(move |size| { - view_content(id, total_panes, pane.is_pinned, size) - })) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_click(Message::Clicked) - .on_drag(Message::Dragged) - .on_resize(10, Message::Resized); - - container(pane_grid) - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .into() - } -} - -const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0xC7 as f32 / 255.0, - 0xC7 as f32 / 255.0, -); -const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0x47 as f32 / 255.0, - 0x47 as f32 / 255.0, -); - -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { - 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| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - }; - - let mut controls = column() - .spacing(5) - .max_width(150) - .push(button( - "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), - )) - .push(button( - "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), - )); - - if total_panes > 1 && !is_pinned { - controls = controls.push( - button("Close", Message::Close(pane)) - .style(theme::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(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); - } - - button.into() -} - -mod style { - use iced::{container, Theme}; - - pub fn title_bar_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), - ..Default::default() - } - } - - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.primary.strong.text), - background: Some(palette.primary.strong.color.into()), - ..Default::default() - } - } - - pub fn pane_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.background.strong.color, - ..Default::default() - } - } - - pub fn pane_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.primary.strong.color, - ..Default::default() - } - } -} diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml deleted file mode 100644 index c0fcac3c4c..0000000000 --- a/examples/pure/pick_list/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[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/src/main.rs b/examples/pure/pick_list/src/main.rs deleted file mode 100644 index b9947107c1..0000000000 --- a/examples/pure/pick_list/src/main.rs +++ /dev/null @@ -1,109 +0,0 @@ -use iced::pure::{column, container, pick_list, scrollable, vertical_space}; -use iced::pure::{Element, Sandbox}; -use iced::{Alignment, Length, Settings}; - -pub fn main() -> iced::Result { - Example::run(Settings::default()) -} - -#[derive(Default)] -struct Example { - selected_language: Option, -} - -#[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", - } - ) - } -} diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml deleted file mode 100644 index 217179e83d..0000000000 --- a/examples/pure/todos/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[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", "default_system_font", "pure"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -async-std = "1.0" -directories-next = "2.0" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -web-sys = { version = "0.3", features = ["Window", "Storage"] } -wasm-timer = "0.2" diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs deleted file mode 100644 index 9a74ea711e..0000000000 --- a/examples/pure/todos/src/main.rs +++ /dev/null @@ -1,556 +0,0 @@ -use iced::alignment::{self, Alignment}; -use iced::pure::widget::Text; -use iced::pure::{ - button, checkbox, column, container, row, scrollable, text, text_input, - Application, Element, -}; -use iced::theme::{self, Theme}; -use iced::window; -use iced::{Color, Command, Font, Length, Settings}; -use serde::{Deserialize, Serialize}; - -pub fn main() -> iced::Result { - Todos::run(Settings { - window: window::Settings { - size: (500, 800), - ..window::Settings::default() - }, - ..Settings::default() - }) -} - -#[derive(Debug)] -enum Todos { - Loading, - Loaded(State), -} - -#[derive(Debug, Default)] -struct State { - input_value: String, - filter: Filter, - tasks: Vec, - 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 Message = Message; - type Theme = Theme; - type Executor = iced::executor::Default; - 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) - .style(Color::from([0.5, 0.5, 0.5])) - .horizontal_alignment(alignment::Horizontal::Center); - - let input = text_input( - "What needs to be done?", - input_value, - Message::InputChanged, - ) - .padding(15) - .size(30) - .on_submit(Message::CreateTask); - - let controls = view_controls(tasks, *filter); - let filtered_tasks = - tasks.iter().filter(|task| filter.matches(task)); - - let tasks: Element<_> = if filtered_tasks.count() > 0 { - tasks - .iter() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(column().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) - .into() - } else { - empty_message(match filter { - Filter::All => "You have not created a task yet...", - Filter::Active => "All your tasks are done! :D", - Filter::Completed => { - "You have not completed a task yet..." - } - }) - }; - - let content = column() - .spacing(20) - .max_width(800) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - scrollable( - container(content) - .width(Length::Fill) - .padding(40) - .center_x(), - ) - .into() - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Task { - description: String, - completed: bool, - - #[serde(skip)] - state: TaskState, -} - -#[derive(Debug, Clone)] -pub enum TaskState { - Idle, - Editing, -} - -impl Default for TaskState { - fn default() -> Self { - Self::Idle - } -} - -#[derive(Debug, Clone)] -pub enum TaskMessage { - Completed(bool), - Edit, - DescriptionEdited(String), - FinishEdition, - Delete, -} - -impl Task { - fn new(description: String) -> Self { - Task { - description, - completed: false, - state: TaskState::Idle, - } - } - - fn update(&mut self, message: TaskMessage) { - match message { - TaskMessage::Completed(completed) => { - self.completed = completed; - } - TaskMessage::Edit => { - self.state = TaskState::Editing; - } - TaskMessage::DescriptionEdited(new_description) => { - self.description = new_description; - } - TaskMessage::FinishEdition => { - if !self.description.is_empty() { - self.state = TaskState::Idle; - } - } - TaskMessage::Delete => {} - } - } - - fn view(&self) -> Element { - 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(theme::Button::Text), - ) - .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(theme::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 { - theme::Button::Primary - } else { - theme::Button::Text - }); - - 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) - .style(Color::from([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 { - File, - Format, -} - -#[derive(Debug, Clone)] -enum SaveError { - File, - Write, - Format, -} - -#[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_default() - }; - - 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::File)?; - - file.read_to_string(&mut contents) - .await - .map_err(|_| LoadError::File)?; - - serde_json::from_str(&contents).map_err(|_| LoadError::Format) - } - - async fn save(self) -> Result<(), SaveError> { - use async_std::prelude::*; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::Format)?; - - let path = Self::path(); - - if let Some(dir) = path.parent() { - async_std::fs::create_dir_all(dir) - .await - .map_err(|_| SaveError::File)?; - } - - { - let mut file = async_std::fs::File::create(path) - .await - .map_err(|_| SaveError::File)?; - - file.write_all(json.as_bytes()) - .await - .map_err(|_| SaveError::Write)?; - } - - // 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::File)?; - - let contents = storage - .get_item("state") - .map_err(|_| LoadError::File)? - .ok_or(LoadError::File)?; - - serde_json::from_str(&contents).map_err(|_| LoadError::Format) - } - - async fn save(self) -> Result<(), SaveError> { - let storage = Self::storage().ok_or(SaveError::File)?; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::Format)?; - - storage - .set_item("state", &json) - .map_err(|_| SaveError::Write)?; - - let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; - - Ok(()) - } -} diff --git a/examples/pure/tooltip/Cargo.toml b/examples/pure/tooltip/Cargo.toml deleted file mode 100644 index d84dfb3736..0000000000 --- a/examples/pure/tooltip/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "pure_tooltip" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez ", "Casper Rogild Storm"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/tooltip/src/main.rs b/examples/pure/tooltip/src/main.rs deleted file mode 100644 index e9a6c11146..0000000000 --- a/examples/pure/tooltip/src/main.rs +++ /dev/null @@ -1,76 +0,0 @@ -use iced::pure::widget::tooltip::Position; -use iced::pure::{button, container, tooltip}; -use iced::pure::{Element, Sandbox}; -use iced::theme; -use iced::{Length, Settings}; - -pub fn main() -> iced::Result { - Example::run(Settings::default()) -} - -struct Example { - position: Position, -} - -#[derive(Debug, Clone)] -enum Message { - ChangePosition, -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Self { - position: Position::Bottom, - } - } - - fn title(&self) -> String { - String::from("Tooltip - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::ChangePosition => { - let position = match &self.position { - Position::FollowCursor => Position::Top, - Position::Top => Position::Bottom, - Position::Bottom => Position::Left, - Position::Left => Position::Right, - Position::Right => Position::FollowCursor, - }; - - self.position = position - } - } - } - - fn view(&self) -> Element { - let tooltip = tooltip( - button("Press to change position") - .on_press(Message::ChangePosition), - position_to_text(self.position), - self.position, - ) - .gap(10) - .style(theme::Container::Box); - - container(tooltip) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} - -fn position_to_text<'a>(position: Position) -> &'a str { - match position { - Position::FollowCursor => "Follow Cursor", - Position::Top => "Top", - Position::Bottom => "Bottom", - Position::Left => "Left", - Position::Right => "Right", - } -} diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml deleted file mode 100644 index 8ce5f198ad..0000000000 --- a/examples/pure/tour/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[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/src/main.rs b/examples/pure/tour/src/main.rs deleted file mode 100644 index 05c269c37b..0000000000 --- a/examples/pure/tour/src/main.rs +++ /dev/null @@ -1,664 +0,0 @@ -use iced::alignment; -use iced::pure::widget::{Button, Column, Container, Slider}; -use iced::pure::{ - checkbox, column, container, horizontal_space, image, radio, row, - scrollable, slider, text, text_input, toggler, vertical_space, -}; -use iced::pure::{Element, Sandbox}; -use iced::theme; -use iced::{Color, Length, Renderer, 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(theme::Button::Secondary), - ); - } - - controls = controls.push(horizontal_space(Length::Fill)); - - if steps.can_continue() { - controls = controls.push( - button("Next") - .on_press(Message::NextPressed) - .style(theme::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 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)).style(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)) - .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!") - .style(Color::from([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, Renderer> { - 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, -} diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 3e9ba9213d..6f487e4cbd 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,9 +1,6 @@ -use iced::qr_code::{self, QRCode}; -use iced::text_input::{self, TextInput}; -use iced::{ - Alignment, Color, Column, Container, Element, Length, Sandbox, Settings, - Text, -}; +use iced::widget::qr_code::{self, QRCode}; +use iced::widget::{column, container, text, text_input}; +use iced::{Alignment, Color, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { QRGenerator::run(Settings::default()) @@ -12,7 +9,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct QRGenerator { data: String, - input: text_input::State, qr_code: Option, } @@ -46,13 +42,12 @@ impl Sandbox for QRGenerator { } } - fn view(&mut self) -> Element { - let title = Text::new("QR Code Generator") + fn view(&self) -> Element { + let title = text("QR Code Generator") .size(70) .style(Color::from([0.5, 0.5, 0.5])); - let input = TextInput::new( - &mut self.input, + let input = text_input( "Type the data of your QR code here...", &self.data, Message::DataChanged, @@ -60,18 +55,16 @@ impl Sandbox for QRGenerator { .size(30) .padding(15); - let mut content = Column::new() + let mut content = column![title, input] .width(Length::Units(700)) .spacing(20) - .align_items(Alignment::Center) - .push(title) - .push(input); + .align_items(Alignment::Center); - if let Some(qr_code) = self.qr_code.as_mut() { + if let Some(qr_code) = self.qr_code.as_ref() { content = content.push(QRCode::new(qr_code).cell_size(10)); } - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index f66d21806f..b7b3dedc28 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,9 +1,9 @@ -use iced::button; -use iced::scrollable; -use iced::{ - Button, Column, Container, Element, Length, ProgressBar, Radio, Row, Rule, - Sandbox, Scrollable, Settings, Space, Text, Theme, +use iced::executor; +use iced::widget::{ + button, column, container, horizontal_rule, progress_bar, radio, + scrollable, text, vertical_space, Row, }; +use iced::{Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) @@ -22,56 +22,72 @@ enum Message { Scrolled(usize, f32), } -impl Sandbox for ScrollableDemo { +impl Application for ScrollableDemo { type Message = Message; - - fn new() -> Self { - ScrollableDemo { - theme: Default::default(), - variants: Variant::all(), - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + ScrollableDemo { + theme: Default::default(), + variants: Variant::all(), + }, + Command::none(), + ) } fn title(&self) -> String { String::from("Scrollable - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { - Message::ThemeChanged(theme) => self.theme = theme, + Message::ThemeChanged(theme) => { + self.theme = theme; + + Command::none() + } Message::ScrollToTop(i) => { if let Some(variant) = self.variants.get_mut(i) { - variant.scrollable.snap_to(0.0); - variant.latest_offset = 0.0; + + scrollable::snap_to(Variant::id(i), 0.0) + } else { + Command::none() } } Message::ScrollToBottom(i) => { if let Some(variant) = self.variants.get_mut(i) { - variant.scrollable.snap_to(1.0); - variant.latest_offset = 1.0; + + scrollable::snap_to(Variant::id(i), 1.0) + } else { + Command::none() } } Message::Scrolled(i, offset) => { if let Some(variant) = self.variants.get_mut(i) { variant.latest_offset = offset; } + + Command::none() } } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let ScrollableDemo { theme, variants, .. } = self; let choose_theme = [Theme::Light, Theme::Dark].iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + column!["Choose a theme:"].spacing(10), |column, option| { - column.push(Radio::new( - *option, + column.push(radio( format!("{:?}", option), + *option, Some(*theme), Message::ThemeChanged, )) @@ -80,88 +96,87 @@ impl Sandbox for ScrollableDemo { let scrollable_row = Row::with_children( variants - .iter_mut() + .iter() .enumerate() .map(|(i, variant)| { - let mut scrollable = - Scrollable::new(&mut variant.scrollable) - .padding(10) - .spacing(10) + let mut contents = column![ + variant.title, + button("Scroll to bottom",) .width(Length::Fill) - .height(Length::Fill) - .on_scroll(move |offset| { - Message::Scrolled(i, offset) - }) - .push(Text::new(variant.title)) - .push( - Button::new( - &mut variant.scroll_to_bottom, - Text::new("Scroll to bottom"), - ) - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToBottom(i)), - ); + .padding(10) + .on_press(Message::ScrollToBottom(i)), + ] + .padding(10) + .spacing(10) + .width(Length::Fill); if let Some(scrollbar_width) = variant.scrollbar_width { - scrollable = scrollable - .scrollbar_width(scrollbar_width) - .push(Text::new(format!( - "scrollbar_width: {:?}", - scrollbar_width - ))); + contents = contents.push(text(format!( + "scrollbar_width: {:?}", + scrollbar_width + ))); } if let Some(scrollbar_margin) = variant.scrollbar_margin { - scrollable = scrollable - .scrollbar_margin(scrollbar_margin) - .push(Text::new(format!( - "scrollbar_margin: {:?}", - scrollbar_margin - ))); + contents = contents.push(text(format!( + "scrollbar_margin: {:?}", + scrollbar_margin + ))); } if let Some(scroller_width) = variant.scroller_width { - scrollable = scrollable - .scroller_width(scroller_width) - .push(Text::new(format!( - "scroller_width: {:?}", - scroller_width - ))); + contents = contents.push(text(format!( + "scroller_width: {:?}", + scroller_width + ))); } - scrollable = scrollable - .push(Space::with_height(Length::Units(100))) - .push(Text::new( + contents = contents + .push(vertical_space(Length::Units(100))) + .push( "Some content that should wrap within the \ scrollable. Let's output a lot of short words, so \ that we'll make sure to see how wrapping works \ with these scrollbars.", - )) - .push(Space::with_height(Length::Units(1200))) - .push(Text::new("Middle")) - .push(Space::with_height(Length::Units(1200))) - .push(Text::new("The End.")) + ) + .push(vertical_space(Length::Units(1200))) + .push("Middle") + .push(vertical_space(Length::Units(1200))) + .push("The End.") .push( - Button::new( - &mut variant.scroll_to_top, - Text::new("Scroll to top"), - ) - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToTop(i)), + button("Scroll to top") + .width(Length::Fill) + .padding(10) + .on_press(Message::ScrollToTop(i)), ); - Column::new() - .width(Length::Fill) + let mut scrollable = scrollable(contents) + .id(Variant::id(i)) .height(Length::Fill) - .spacing(10) - .push(scrollable) - .push(ProgressBar::new( - 0.0..=1.0, - variant.latest_offset, - )) - .into() + .on_scroll(move |offset| Message::Scrolled(i, offset)); + + if let Some(scrollbar_width) = variant.scrollbar_width { + scrollable = + scrollable.scrollbar_width(scrollbar_width); + } + + if let Some(scrollbar_margin) = variant.scrollbar_margin { + scrollable = + scrollable.scrollbar_margin(scrollbar_margin); + } + + if let Some(scroller_width) = variant.scroller_width { + scrollable = scrollable.scroller_width(scroller_width); + } + + column![ + scrollable, + progress_bar(0.0..=1.0, variant.latest_offset,) + ] + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .into() }) .collect(), ) @@ -169,14 +184,12 @@ impl Sandbox for ScrollableDemo { .width(Length::Fill) .height(Length::Fill); - let content = Column::new() - .spacing(20) - .padding(20) - .push(choose_theme) - .push(Rule::horizontal(20)) - .push(scrollable_row); + let content = + column![choose_theme, horizontal_rule(20), scrollable_row] + .spacing(20) + .padding(20); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -192,9 +205,6 @@ impl Sandbox for ScrollableDemo { /// A version of a scrollable struct Variant { title: &'static str, - scrollable: scrollable::State, - scroll_to_top: button::State, - scroll_to_bottom: button::State, scrollbar_width: Option, scrollbar_margin: Option, scroller_width: Option, @@ -206,9 +216,6 @@ impl Variant { vec![ Self { title: "Default Scrollbar", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: None, scrollbar_margin: None, scroller_width: None, @@ -216,9 +223,6 @@ impl Variant { }, Self { title: "Slimmed & Margin", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: Some(3), scroller_width: Some(4), @@ -226,9 +230,6 @@ impl Variant { }, Self { title: "Wide Scroller", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: None, scroller_width: Some(10), @@ -236,9 +237,6 @@ impl Variant { }, Self { title: "Narrow Scroller", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(10), scrollbar_margin: None, scroller_width: Some(4), @@ -246,4 +244,8 @@ impl Variant { }, ] } + + pub fn id(i: usize) -> scrollable::Id { + scrollable::Id::new(format!("scrollable-{}", i)) + } } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index fc53d8f7c2..c59d73a833 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,14 +7,15 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::application; -use iced::canvas::{self, Cursor, Path, Stroke}; use iced::executor; use iced::theme::{self, Theme}; use iced::time; +use iced::widget::canvas; +use iced::widget::canvas::{Cursor, Path, Stroke}; use iced::window; use iced::{ - Application, Canvas, Color, Command, Element, Length, Point, Rectangle, - Settings, Size, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Settings, + Size, Subscription, Vector, }; use std::time::Instant; @@ -68,8 +69,8 @@ impl Application for SolarSystem { time::every(std::time::Duration::from_millis(10)).map(Message::Tick) } - fn view(&mut self) -> Element { - Canvas::new(&mut self.state) + fn view(&self) -> Element { + canvas(&self.state) .width(Length::Fill) .height(Length::Fill) .into() @@ -145,8 +146,11 @@ impl State { } impl canvas::Program for State { + type State = (); + fn draw( &self, + _state: &Self::State, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index b7c816ff3e..b8cee807e0 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,11 +1,10 @@ use iced::alignment; -use iced::button; use iced::executor; use iced::theme::{self, Theme}; use iced::time; +use iced::widget::{button, column, container, row, text}; use iced::{ - Alignment, Application, Button, Column, Command, Container, Element, - Length, Row, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, }; use std::time::{Duration, Instant}; @@ -17,8 +16,6 @@ pub fn main() -> iced::Result { struct Stopwatch { duration: Duration, state: State, - toggle: button::State, - reset: button::State, } enum State { @@ -44,8 +41,6 @@ impl Application for Stopwatch { Stopwatch { duration: Duration::default(), state: State::Idle, - toggle: button::State::new(), - reset: button::State::new(), }, Command::none(), ) @@ -90,13 +85,13 @@ impl Application for Stopwatch { } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { const MINUTE: u64 = 60; const HOUR: u64 = 60 * MINUTE; let seconds = self.duration.as_secs(); - let duration = Text::new(format!( + let duration = text(format!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, (seconds % HOUR) / MINUTE, @@ -105,11 +100,9 @@ impl Application for Stopwatch { )) .size(40); - let button = |state, label| { - Button::new( - state, - Text::new(label) - .horizontal_alignment(alignment::Horizontal::Center), + let button = |label| { + button( + text(label).horizontal_alignment(alignment::Horizontal::Center), ) .padding(10) .width(Length::Units(80)) @@ -121,25 +114,20 @@ impl Application for Stopwatch { State::Ticking { .. } => "Stop", }; - button(&mut self.toggle, label).on_press(Message::Toggle) + button(label).on_press(Message::Toggle) }; - let reset_button = button(&mut self.reset, "Reset") + let reset_button = button("Reset") .style(theme::Button::Destructive) .on_press(Message::Reset); - let controls = Row::new() - .spacing(20) - .push(toggle_button) - .push(reset_button); + let controls = row![toggle_button, reset_button].spacing(20); - let content = Column::new() + let content = column![duration, controls] .align_items(Alignment::Center) - .spacing(20) - .push(duration) - .push(controls); + .spacing(20); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index aa90d17c3a..cda53e8796 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,12 +1,9 @@ -use iced::button; -use iced::scrollable; -use iced::slider; -use iced::text_input; -use iced::{ - Alignment, Button, Checkbox, Column, Container, Element, Length, - ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Slider, - Space, Text, TextInput, Theme, Toggler, +use iced::widget::{ + button, checkbox, column, container, horizontal_rule, progress_bar, radio, + row, scrollable, slider, text, text_input, toggler, vertical_rule, + vertical_space, }; +use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; pub fn main() -> iced::Result { Styling::run(Settings::default()) @@ -15,11 +12,7 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Styling { theme: Theme, - scroll: scrollable::State, - input: text_input::State, input_value: String, - button: button::State, - slider: slider::State, slider_value: f32, checkbox_value: bool, toggler_value: bool, @@ -57,21 +50,20 @@ impl Sandbox for Styling { } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let choose_theme = [Theme::Light, Theme::Dark].iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + column![text("Choose a theme:")].spacing(10), |column, theme| { - column.push(Radio::new( - *theme, + column.push(radio( format!("{:?}", theme), + *theme, Some(self.theme), Message::ThemeChanged, )) }, ); - let text_input = TextInput::new( - &mut self.input, + let text_input = text_input( "Type something...", &self.input_value, Message::InputChanged, @@ -79,66 +71,59 @@ impl Sandbox for Styling { .padding(10) .size(20); - let button = Button::new(&mut self.button, Text::new("Submit")) + let button = button("Submit") .padding(10) .on_press(Message::ButtonPressed); - let slider = Slider::new( - &mut self.slider, - 0.0..=100.0, - self.slider_value, - Message::SliderChanged, - ); + let slider = + slider(0.0..=100.0, self.slider_value, Message::SliderChanged); - let progress_bar = ProgressBar::new(0.0..=100.0, self.slider_value); + let progress_bar = progress_bar(0.0..=100.0, self.slider_value); - let scrollable = Scrollable::new(&mut self.scroll) - .width(Length::Fill) - .height(Length::Units(100)) - .push(Text::new("Scroll me!")) - .push(Space::with_height(Length::Units(800))) - .push(Text::new("You did it!")); + let scrollable = scrollable( + column![ + "Scroll me!", + vertical_space(Length::Units(800)), + "You did it!" + ] + .width(Length::Fill), + ) + .height(Length::Units(100)); - let checkbox = Checkbox::new( - self.checkbox_value, + let checkbox = checkbox( "Check me!", + self.checkbox_value, Message::CheckboxToggled, ); - let toggler = Toggler::new( - self.toggler_value, + let toggler = toggler( String::from("Toggle me!"), + self.toggler_value, Message::TogglerToggled, ) .width(Length::Shrink) .spacing(10); - let content = Column::new() - .spacing(20) - .padding(20) - .max_width(600) - .push(choose_theme) - .push(Rule::horizontal(38)) - .push(Row::new().spacing(10).push(text_input).push(button)) - .push(slider) - .push(progress_bar) - .push( - Row::new() - .spacing(10) - .height(Length::Units(100)) - .align_items(Alignment::Center) - .push(scrollable) - .push(Rule::vertical(38)) - .push( - Column::new() - .width(Length::Shrink) - .spacing(20) - .push(checkbox) - .push(toggler), - ), - ); - - Container::new(content) + let content = column![ + choose_theme, + horizontal_rule(38), + row![text_input, button].spacing(10), + slider, + progress_bar, + row![ + scrollable, + vertical_rule(38), + column![checkbox, toggler].spacing(20) + ] + .spacing(10) + .height(Length::Units(100)) + .align_items(Alignment::Center), + ] + .spacing(20) + .padding(20) + .max_width(600); + + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 8707fa3b67..27d175da7d 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,4 +1,5 @@ -use iced::{Container, Element, Length, Sandbox, Settings, Svg}; +use iced::widget::{container, svg}; +use iced::{Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Tiger::run(Settings::default()) @@ -19,15 +20,15 @@ impl Sandbox for Tiger { fn update(&mut self, _message: ()) {} - fn view(&mut self) -> Element<()> { - let svg = Svg::from_path(format!( + fn view(&self) -> Element<()> { + let svg = svg(svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") - )) + ))) .width(Length::Fill) .height(Length::Fill); - Container::new(svg) + container(svg) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 54fc635fa3..af67742f99 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -1,6 +1,6 @@ +use iced::widget::{button, column, container, text}; use iced::{ - button, executor, system, Application, Button, Column, Command, Container, - Element, Length, Settings, Text, Theme, + executor, system, Application, Command, Element, Length, Settings, Theme, }; use bytesize::ByteSize; @@ -11,10 +11,7 @@ pub fn main() -> iced::Result { enum Example { Loading, - Loaded { - information: system::Information, - refresh_button: button::State, - }, + Loaded { information: system::Information }, } #[derive(Clone, Debug)] @@ -48,25 +45,18 @@ impl Application for Example { return system::fetch_information(Message::InformationReceived); } Message::InformationReceived(information) => { - let refresh_button = button::State::new(); - *self = Self::Loaded { - information, - refresh_button, - }; + *self = Self::Loaded { information }; } } Command::none() } - fn view(&mut self) -> Element { - let content: Element = match self { - Example::Loading => Text::new("Loading...").size(40).into(), - Example::Loaded { - information, - refresh_button, - } => { - let system_name = Text::new(format!( + fn view(&self) -> Element { + let content: Element<_> = match self { + Example::Loading => text("Loading...").size(40).into(), + Example::Loaded { information } => { + let system_name = text(format!( "System name: {}", information .system_name @@ -74,7 +64,7 @@ impl Application for Example { .unwrap_or(&"unknown".to_string()) )); - let system_kernel = Text::new(format!( + let system_kernel = text(format!( "System kernel: {}", information .system_kernel @@ -82,7 +72,7 @@ impl Application for Example { .unwrap_or(&"unknown".to_string()) )); - let system_version = Text::new(format!( + let system_version = text(format!( "System version: {}", information .system_version @@ -90,12 +80,10 @@ impl Application for Example { .unwrap_or(&"unknown".to_string()) )); - let cpu_brand = Text::new(format!( - "Processor brand: {}", - information.cpu_brand - )); + let cpu_brand = + text(format!("Processor brand: {}", information.cpu_brand)); - let cpu_cores = Text::new(format!( + let cpu_cores = text(format!( "Processor cores: {}", information .cpu_cores @@ -106,7 +94,7 @@ impl Application for Example { let memory_readable = ByteSize::kb(information.memory_total).to_string(); - let memory_total = Text::new(format!( + let memory_total = text(format!( "Memory (total): {} kb ({})", information.memory_total, memory_readable )); @@ -122,38 +110,36 @@ impl Application for Example { }; let memory_used = - Text::new(format!("Memory (used): {}", memory_text)); + text(format!("Memory (used): {}", memory_text)); - let graphics_adapter = Text::new(format!( + let graphics_adapter = text(format!( "Graphics adapter: {}", information.graphics_adapter )); - let graphics_backend = Text::new(format!( + let graphics_backend = text(format!( "Graphics backend: {}", information.graphics_backend )); - Column::with_children(vec![ - system_name.size(30).into(), - system_kernel.size(30).into(), - system_version.size(30).into(), - cpu_brand.size(30).into(), - cpu_cores.size(30).into(), - memory_total.size(30).into(), - memory_used.size(30).into(), - graphics_adapter.size(30).into(), - graphics_backend.size(30).into(), - Button::new(refresh_button, Text::new("Refresh")) - .on_press(Message::Refresh) - .into(), - ]) + column![ + system_name.size(30), + system_kernel.size(30), + system_version.size(30), + cpu_brand.size(30), + cpu_cores.size(30), + memory_total.size(30), + memory_used.size(30), + graphics_adapter.size(30), + graphics_backend.size(30), + button("Refresh").on_press(Message::Refresh) + ] .spacing(10) .into() } }; - Container::new(content) + container(content) .center_x() .center_y() .width(Length::Fill) diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5b068c789f..2326ffc6e1 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +lazy_static = "1.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 363e3fb802..bb00aac67e 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,16 +1,31 @@ use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; -use iced::scrollable::{self, Scrollable}; -use iced::text_input::{self, TextInput}; +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; use iced::theme::{self, Theme}; -use iced::{ - Application, Checkbox, Color, Column, Command, Container, Element, Font, - Length, Row, Settings, Text, +use iced::widget::{ + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, }; +use iced::window; +use iced::{Application, Element}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; + +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +lazy_static! { + static ref INPUT_ID: text_input::Id = text_input::Id::unique(); +} + 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)] @@ -21,12 +36,9 @@ enum Todos { #[derive(Debug, Default)] struct State { - scroll: scrollable::State, - input: text_input::State, input_value: String, filter: Filter, tasks: Vec, - controls: Controls, dirty: bool, saving: bool, } @@ -39,12 +51,13 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, } impl Application for Todos { - type Executor = iced::executor::Default; - type Theme = Theme; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Todos, Command) { @@ -81,14 +94,16 @@ impl Application for Todos { _ => {} } - Command::none() + text_input::focus(INPUT_ID.clone()) } Todos::Loaded(state) => { let mut saved = false; - match message { + let command = match message { Message::InputChanged(value) => { state.input_value = value; + + Command::none() } Message::CreateTask => { if !state.input_value.is_empty() { @@ -97,30 +112,56 @@ impl Application for Todos { .push(Task::new(state.input_value.clone())); state.input_value.clear(); } + + Command::none() } Message::FilterChanged(filter) => { state.filter = filter; + + Command::none() } Message::TaskMessage(i, TaskMessage::Delete) => { state.tasks.remove(i); + + Command::none() } Message::TaskMessage(i, task_message) => { if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + task.update(task_message); + + if should_focus { + text_input::focus(Task::text_input_id(i)) + } else { + Command::none() + } + } else { + Command::none() } } Message::Saved(_) => { state.saving = false; saved = true; + + Command::none() } - _ => {} - } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + _ => Command::none(), + }; if !saved { state.dirty = true; } - if state.dirty && !state.saving { + let save = if state.dirty && !state.saving { state.dirty = false; state.saving = true; @@ -135,54 +176,57 @@ impl Application for Todos { ) } else { Command::none() - } + }; + + Command::batch(vec![command, save]) } } } - fn view(&mut self) -> Element { + fn view(&self) -> Element { match self { Todos::Loading => loading_message(), Todos::Loaded(State { - scroll, - input, input_value, filter, tasks, - controls, .. }) => { - let title = Text::new("todos") + let title = text("todos") .width(Length::Fill) .size(100) .style(Color::from([0.5, 0.5, 0.5])) .horizontal_alignment(alignment::Horizontal::Center); - let input = TextInput::new( - input, + let input = text_input( "What needs to be done?", input_value, Message::InputChanged, ) + .id(INPUT_ID.clone()) .padding(15) .size(30) .on_submit(Message::CreateTask); - let controls = controls.view(tasks, *filter); + 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_mut() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(Column::new().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) - .into() + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect(), + ) + .spacing(10) + .into() } else { empty_message(match filter { Filter::All => "You have not created a task yet...", @@ -193,23 +237,36 @@ impl Application for Todos { }) }; - let content = Column::new() - .max_width(800) + let content = column![title, input, controls, tasks] .spacing(20) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - Scrollable::new(scroll) - .padding(40) - .push( - Container::new(content).width(Length::Fill).center_x(), - ) - .into() + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() } } } + + fn subscription(&self) -> Subscription { + subscription::events_with(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + _ => None, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -223,20 +280,13 @@ struct Task { #[derive(Debug, Clone)] pub enum TaskState { - Idle { - edit_button: button::State, - }, - Editing { - text_input: text_input::State, - delete_button: button::State, - }, + Idle, + Editing, } impl Default for TaskState { fn default() -> Self { - TaskState::Idle { - edit_button: button::State::new(), - } + Self::Idle } } @@ -250,13 +300,15 @@ pub enum TaskMessage { } impl Task { + fn text_input_id(i: usize) -> text_input::Id { + text_input::Id::new(format!("task-{}", i)) + } + fn new(description: String) -> Self { Task { description, completed: false, - state: TaskState::Idle { - edit_button: button::State::new(), - }, + state: TaskState::Idle, } } @@ -266,150 +318,100 @@ impl Task { self.completed = completed; } TaskMessage::Edit => { - let mut text_input = text_input::State::focused(); - text_input.select_all(); - - self.state = TaskState::Editing { - text_input, - delete_button: button::State::new(), - }; + self.state = TaskState::Editing; } TaskMessage::DescriptionEdited(new_description) => { self.description = new_description; } TaskMessage::FinishEdition => { if !self.description.is_empty() { - self.state = TaskState::Idle { - edit_button: button::State::new(), - } + self.state = TaskState::Idle; } } TaskMessage::Delete => {} } } - fn view(&mut self) -> Element { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, + fn view(&self, i: usize) -> Element { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( &self.description, + self.completed, TaskMessage::Completed, ) .width(Length::Fill); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(checkbox) - .push( - Button::new(edit_button, edit_icon()) - .on_press(TaskMessage::Edit) - .padding(10) - .style(theme::Button::Text), - ) - .into() + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(theme::Button::Text), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } - TaskState::Editing { - text_input, - delete_button, - } => { - let text_input = TextInput::new( - text_input, + TaskState::Editing => { + let text_input = text_input( "Describe your task...", &self.description, TaskMessage::DescriptionEdited, ) + .id(Self::text_input_id(i)) .on_submit(TaskMessage::FinishEdition) .padding(10); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(text_input) - .push( - Button::new( - delete_button, - Row::new() - .spacing(10) - .push(delete_icon()) - .push(Text::new("Delete")), - ) + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) .on_press(TaskMessage::Delete) .padding(10) - .style(theme::Button::Destructive), - ) - .into() + .style(theme::Button::Destructive) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } } } } -#[derive(Debug, Default, Clone)] -pub struct Controls { - all_button: button::State, - active_button: button::State, - completed_button: button::State, -} - -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row { - let Controls { - all_button, - active_button, - completed_button, - } = self; - - let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - - let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16); - let button = - Button::new(state, label).style(if filter == current_filter { - theme::Button::Primary - } else { - theme::Button::Text - }); +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - button.on_press(Message::FilterChanged(filter)).padding(8) - }; + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push( - Text::new(format!( - "{} {} left", - tasks_left, - if tasks_left == 1 { "task" } else { "tasks" } - )) - .width(Length::Fill) - .size(16), - ) - .push( - Row::new() - .width(Length::Shrink) - .spacing(10) - .push(filter_button( - all_button, - "All", - Filter::All, - current_filter, - )) - .push(filter_button( - active_button, - "Active", - Filter::Active, - current_filter, - )) - .push(filter_button( - completed_button, - "Completed", - Filter::Completed, - current_filter, - )), - ) - } + let button = button(label).style(if filter == current_filter { + theme::Button::Primary + } else { + theme::Button::Text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -436,8 +438,8 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - Container::new( - Text::new("Loading...") + container( + text("Loading...") .horizontal_alignment(alignment::Horizontal::Center) .size(50), ) @@ -447,9 +449,9 @@ fn loading_message<'a>() -> Element<'a, Message> { .into() } -fn empty_message<'a>(message: &str) -> Element<'a, Message> { - Container::new( - Text::new(message) +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) .width(Length::Fill) .size(25) .horizontal_alignment(alignment::Horizontal::Center) @@ -464,11 +466,11 @@ fn empty_message<'a>(message: &str) -> Element<'a, Message> { // Fonts const ICONS: Font = Font::External { name: "Icons", - bytes: include_bytes!("../fonts/icons.ttf"), + bytes: include_bytes!("../../todos/fonts/icons.ttf"), }; fn icon(unicode: char) -> Text { - Text::new(unicode.to_string()) + text(unicode.to_string()) .font(ICONS) .width(Length::Units(20)) .horizontal_alignment(alignment::Horizontal::Center) diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index 7c8001128f..35b862a832 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -1,117 +1,75 @@ -use iced::alignment::{self, Alignment}; -use iced::button; use iced::theme; -use iced::tooltip::{self, Tooltip}; -use iced::{ - Button, Column, Container, Element, Length, Row, Sandbox, Settings, Text, -}; +use iced::widget::tooltip::Position; +use iced::widget::{button, container, tooltip}; +use iced::{Element, Length, Sandbox, Settings}; -pub fn main() { - Example::run(Settings::default()).unwrap() +pub fn main() -> iced::Result { + Example::run(Settings::default()) } -#[derive(Default)] struct Example { - top: button::State, - bottom: button::State, - right: button::State, - left: button::State, - follow_cursor: button::State, + position: Position, } -#[derive(Debug, Clone, Copy)] -struct Message; +#[derive(Debug, Clone)] +enum Message { + ChangePosition, +} impl Sandbox for Example { type Message = Message; fn new() -> Self { - Self::default() + Self { + position: Position::Bottom, + } } fn title(&self) -> String { String::from("Tooltip - Iced") } - fn update(&mut self, _message: Message) {} - - fn view(&mut self) -> Element { - let top = - tooltip("Tooltip at top", &mut self.top, tooltip::Position::Top); - - let bottom = tooltip( - "Tooltip at bottom", - &mut self.bottom, - tooltip::Position::Bottom, - ); - - let left = - tooltip("Tooltip at left", &mut self.left, tooltip::Position::Left); - - let right = tooltip( - "Tooltip at right", - &mut self.right, - tooltip::Position::Right, - ); - - let fixed_tooltips = Row::with_children(vec![top, bottom, left, right]) - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::Center) - .spacing(50); - - let follow_cursor = tooltip( - "Tooltip follows cursor", - &mut self.follow_cursor, - tooltip::Position::FollowCursor, - ); + fn update(&mut self, message: Message) { + match message { + Message::ChangePosition => { + let position = match &self.position { + Position::FollowCursor => Position::Top, + Position::Top => Position::Bottom, + Position::Bottom => Position::Left, + Position::Left => Position::Right, + Position::Right => Position::FollowCursor, + }; + + self.position = position + } + } + } - let content = Column::with_children(vec![ - Container::new(fixed_tooltips) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into(), - follow_cursor, - ]) - .width(Length::Fill) - .height(Length::Fill) - .spacing(50); + fn view(&self) -> Element { + let tooltip = tooltip( + button("Press to change position") + .on_press(Message::ChangePosition), + position_to_text(self.position), + self.position, + ) + .gap(10) + .style(theme::Container::Box); - Container::new(content) + container(tooltip) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .padding(50) .into() } } -fn tooltip<'a>( - label: &str, - button_state: &'a mut button::State, - position: tooltip::Position, -) -> Element<'a, Message> { - Tooltip::new( - Button::new( - button_state, - Text::new(label) - .size(40) - .width(Length::Fill) - .height(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), - ) - .on_press(Message) - .width(Length::Fill) - .height(Length::Fill), - "Tooltip", - position, - ) - .gap(5) - .padding(10) - .style(theme::Container::Box) - .into() +fn position_to_text<'a>(position: Position) -> &'a str { + match position { + Position::FollowCursor => "Follow Cursor", + Position::Top => "Top", + Position::Bottom => "Bottom", + Position::Left => "Left", + Position::Right => "Right", + } } diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index d85f2916f4..82dac0cd97 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,14 +1,11 @@ use iced::alignment; -use iced::button; -use iced::scrollable; -use iced::slider; -use iced::text_input; use iced::theme; -use iced::{ - Button, Checkbox, Color, Column, Container, ContentFit, Element, Image, - Length, Radio, Row, Sandbox, Scrollable, Settings, Slider, Space, Text, - TextInput, Toggler, +use iced::widget::{ + checkbox, column, container, horizontal_space, image, radio, row, + scrollable, slider, text, text_input, toggler, vertical_space, }; +use iced::widget::{Button, Column, Container, Slider}; +use iced::{Color, Element, Length, Renderer, Sandbox, Settings}; pub fn main() -> iced::Result { env_logger::init(); @@ -18,9 +15,6 @@ pub fn main() -> iced::Result { pub struct Tour { steps: Steps, - scroll: scrollable::State, - back_button: button::State, - next_button: button::State, debug: bool, } @@ -30,9 +24,6 @@ impl Sandbox for Tour { fn new() -> Tour { Tour { steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), debug: false, } } @@ -55,56 +46,41 @@ impl Sandbox for Tour { } } - fn view(&mut self) -> Element { - let Tour { - steps, - scroll, - back_button, - next_button, - .. - } = self; + fn view(&self) -> Element { + let Tour { steps, .. } = self; - let mut controls = Row::new(); + let mut controls = row![]; if steps.has_previous() { controls = controls.push( - button(back_button, "Back") + button("Back") .on_press(Message::BackPressed) .style(theme::Button::Secondary), ); } - controls = controls.push(Space::with_width(Length::Fill)); + controls = controls.push(horizontal_space(Length::Fill)); if steps.can_continue() { controls = controls.push( - button(next_button, "Next") + button("Next") .on_press(Message::NextPressed) .style(theme::Button::Primary), ); } - let content: Element<_> = Column::new() - .max_width(540) - .spacing(20) - .padding(20) - .push(steps.view(self.debug).map(Message::StepMessage)) - .push(controls) - .into(); - - let content = if self.debug { - content.explain(Color::BLACK) - } else { - content - }; + let content = column![ + steps.view(self.debug).map(Message::StepMessage), + controls, + ] + .max_width(540) + .spacing(20) + .padding(20); - let scrollable = Scrollable::new(scroll) - .push(Container::new(content).width(Length::Fill).center_x()); + let scrollable = + scrollable(container(content).width(Length::Fill).center_x()); - Container::new(scrollable) - .height(Length::Fill) - .center_y() - .into() + container(scrollable).height(Length::Fill).center_y().into() } } @@ -125,35 +101,24 @@ impl Steps { Steps { steps: vec![ Step::Welcome, - Step::Slider { - state: slider::State::new(), - value: 50, - }, + Step::Slider { value: 50 }, Step::RowsAndColumns { layout: Layout::Row, - spacing_slider: slider::State::new(), spacing: 20, }, Step::Text { - size_slider: slider::State::new(), size: 30, - color_sliders: [slider::State::new(); 3], - color: Color::from_rgb(0.5, 0.5, 0.5), + color: Color::BLACK, }, Step::Radio { selection: None }, Step::Toggler { can_continue: false, }, - Step::Image { - height: 200, - current_fit: ContentFit::Contain, - slider: slider::State::new(), - }, + Step::Image { width: 300 }, Step::Scrollable, Step::TextInput { value: String::new(), is_secure: false, - state: text_input::State::new(), }, Step::Debugger, Step::End, @@ -166,7 +131,7 @@ impl Steps { self.steps[self.current].update(msg, debug); } - fn view(&mut self, debug: bool) -> Element { + fn view(&self, debug: bool) -> Element { self.steps[self.current].view(debug) } @@ -198,38 +163,14 @@ impl Steps { enum Step { Welcome, - Slider { - state: slider::State, - value: u8, - }, - RowsAndColumns { - layout: Layout, - spacing_slider: slider::State, - spacing: u16, - }, - Text { - size_slider: slider::State, - size: u16, - color_sliders: [slider::State; 3], - color: Color, - }, - Radio { - selection: Option, - }, - Toggler { - can_continue: bool, - }, - Image { - height: u16, - slider: slider::State, - current_fit: ContentFit, - }, + 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, - state: text_input::State, - }, + TextInput { value: String, is_secure: bool }, Debugger, End, } @@ -242,8 +183,7 @@ pub enum StepMessage { TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageHeightChanged(u16), - ImageFitSelected(ContentFit), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -288,14 +228,9 @@ impl<'a> Step { *spacing = new_spacing; } } - StepMessage::ImageHeightChanged(new_height) => { - if let Step::Image { height, .. } = self { - *height = new_height; - } - } - StepMessage::ImageFitSelected(fit) => { - if let Step::Image { current_fit, .. } = self { - *current_fit = fit; + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -348,34 +283,21 @@ impl<'a> Step { } } - fn view(&mut self, debug: bool) -> Element { + 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 { state, value } => Self::slider(state, *value), - Step::Text { - size_slider, - size, - color_sliders, - color, - } => Self::text(size_slider, *size, color_sliders, *color), - Step::Image { - height, - slider, - current_fit, - } => Self::image(*height, slider, *current_fit), - Step::RowsAndColumns { - layout, - spacing_slider, - spacing, - } => Self::rows_and_columns(*layout, spacing_slider, *spacing), + 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, - state, - } => Self::text_input(value, *is_secure, state), + Step::TextInput { value, is_secure } => { + Self::text_input(value, *is_secure) + } Step::Debugger => Self::debugger(debug), Step::End => Self::end(), } @@ -383,59 +305,51 @@ impl<'a> Step { } fn container(title: &str) -> Column<'a, StepMessage> { - Column::new().spacing(20).push(Text::new(title).size(50)) + column![text(title).size(50)].spacing(20) } fn welcome() -> Column<'a, StepMessage> { Self::container("Welcome!") - .push(Text::new( + .push( "This is a simple tour meant to showcase a bunch of widgets \ that can be easily implemented on top of Iced.", - )) - .push(Text::new( + ) + .push( "Iced is a cross-platform GUI library for Rust focused on \ simplicity and type-safety. It is heavily inspired by Elm.", - )) - .push(Text::new( + ) + .push( "It was originally born as part of Coffee, an opinionated \ 2D game engine for Rust.", - )) - .push(Text::new( + ) + .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(Text::new( + ) + .push( "Additionally, this tour can also run on WebAssembly thanks \ to dodrio, an experimental VDOM library for Rust.", - )) - .push(Text::new( + ) + .push( "You will need to interact with the UI in order to reach the \ end!", - )) + ) } - fn slider( - state: &'a mut slider::State, - value: u8, - ) -> Column<'a, StepMessage> { + fn slider(value: u8) -> Column<'a, StepMessage> { Self::container("Slider") - .push(Text::new( + .push( "A slider allows you to smoothly select a value from a range \ of values.", - )) - .push(Text::new( + ) + .push( "The following slider lets you choose an integer from \ 0 to 100:", - )) - .push(Slider::new( - state, - 0..=100, - value, - StepMessage::SliderChanged, - )) + ) + .push(slider(0..=100, value, StepMessage::SliderChanged)) .push( - Text::new(value.to_string()) + text(value.to_string()) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) @@ -443,257 +357,198 @@ impl<'a> Step { fn rows_and_columns( layout: Layout, - spacing_slider: &'a mut slider::State, spacing: u16, ) -> Column<'a, StepMessage> { - let row_radio = Radio::new( - Layout::Row, - "Row", - Some(layout), - StepMessage::LayoutChanged, - ); + let row_radio = + radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); - let column_radio = Radio::new( - Layout::Column, + let column_radio = radio( "Column", + Layout::Column, Some(layout), StepMessage::LayoutChanged, ); let layout_section: Element<_> = match layout { - Layout::Row => Row::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - Layout::Column => Column::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), + Layout::Row => { + row![row_radio, column_radio].spacing(spacing).into() + } + Layout::Column => { + column![row_radio, column_radio].spacing(spacing).into() + } }; - let spacing_section = Column::new() - .spacing(10) - .push(Slider::new( - spacing_slider, - 0..=80, - spacing, - StepMessage::SpacingChanged, - )) - .push( - Text::new(format!("{} px", spacing)) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ); + let spacing_section = column![ + slider(0..=80, spacing, StepMessage::SpacingChanged), + text(format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ] + .spacing(10); Self::container("Rows and columns") .spacing(spacing) - .push(Text::new( + .push( "Iced uses a layout model based on flexbox to position UI \ elements.", - )) - .push(Text::new( + ) + .push( "Rows and columns can be used to distribute content \ horizontally or vertically, respectively.", - )) + ) .push(layout_section) - .push(Text::new( - "You can also easily change the spacing between elements:", - )) + .push("You can also easily change the spacing between elements:") .push(spacing_section) } - fn text( - size_slider: &'a mut slider::State, - size: u16, - color_sliders: &'a mut [slider::State; 3], - color: Color, - ) -> Column<'a, StepMessage> { - let size_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("You can change its size:")) - .push(Text::new(format!("This text is {} pixels", size)).size(size)) - .push(Slider::new( - size_slider, - 10..=70, - size, - StepMessage::TextSizeChanged, - )); - - let [red, green, blue] = color_sliders; - - let color_sliders = Row::new() - .spacing(10) - .push(color_slider(red, color.r, move |r| Color { r, ..color })) - .push(color_slider(green, color.g, move |g| Color { g, ..color })) - .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + let size_section = column![ + "You can change its size:", + text(format!("This text is {} pixels", size)).size(size), + slider(10..=70, size, StepMessage::TextSizeChanged), + ] + .padding(20) + .spacing(20); - let color_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("And its color:")) - .push(Text::new(format!("{:?}", color)).style(color)) - .push(color_sliders); + let color_sliders = row![ + color_slider(color.r, move |r| Color { r, ..color }), + color_slider(color.g, move |g| Color { g, ..color }), + color_slider(color.b, move |b| Color { b, ..color }), + ] + .spacing(10); + + let color_section = column![ + "And its color:", + text(format!("{:?}", color)).style(color), + color_sliders, + ] + .padding(20) + .spacing(20); Self::container("Text") - .push(Text::new( + .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::new() - .padding(20) + let question = column![ + text("Iced is written in...").size(24), + column( + Language::all() + .iter() + .cloned() + .map(|language| { + radio( + language, + language, + selection, + StepMessage::LanguageSelected, + ) + }) + .map(Element::from) + .collect() + ) .spacing(10) - .push(Text::new("Iced is written in...").size(24)) - .push(Language::all().iter().cloned().fold( - Column::new().padding(10).spacing(20), - |choices, language| { - choices.push(Radio::new( - language, - language, - selection, - StepMessage::LanguageSelected, - )) - }, - )); + ] + .padding(20) + .spacing(10); Self::container("Radio button") - .push(Text::new( + .push( "A radio button is normally used to represent a choice... \ Surprise test!", - )) + ) .push(question) - .push(Text::new( + .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(Text::new( - "A toggler is mostly used to enable or disable something.", - )) + .push("A toggler is mostly used to enable or disable something.") .push( - Container::new(Toggler::new( + Container::new(toggler( + "Toggle me to continue...".to_owned(), can_continue, - String::from("Toggle me to continue..."), StepMessage::TogglerChanged, )) .padding([0, 40]), ) } - fn image( - height: u16, - slider: &'a mut slider::State, - current_fit: ContentFit, - ) -> Column<'a, StepMessage> { - const FIT_MODES: [(ContentFit, &str); 3] = [ - (ContentFit::Contain, "Contain"), - (ContentFit::Cover, "Cover"), - (ContentFit::Fill, "Fill"), - ]; - - let mode_selector = FIT_MODES.iter().fold( - Column::new().padding(10).spacing(20), - |choices, (mode, name)| { - choices.push(Radio::new( - *mode, - *name, - Some(current_fit), - StepMessage::ImageFitSelected, - )) - }, - ); - + fn image(width: u16) -> Column<'a, StepMessage> { Self::container("Image") - .push(Text::new("Pictures of things in all shapes and sizes!")) - .push(ferris(height, current_fit)) - .push(Slider::new( - slider, - 50..=500, - height, - StepMessage::ImageHeightChanged, - )) + .push("An image that tries to keep its aspect ratio.") + .push(ferris(width)) + .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push( - Text::new(format!("Height: {} px", height)) + text(format!("Width: {} px", width)) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) - .push(Text::new("Pick a content fit strategy:")) - .push(mode_selector) } fn scrollable() -> Column<'a, StepMessage> { Self::container("Scrollable") - .push(Text::new( + .push( "Iced supports scrollable content. Try it out! Find the \ button further below.", - )) + ) .push( - Text::new( - "Tip: You can use the scrollbar to scroll down faster!", - ) - .size(16), + text("Tip: You can use the scrollbar to scroll down faster!") + .size(16), ) - .push(Column::new().height(Length::Units(4096))) + .push(vertical_space(Length::Units(4096))) .push( - Text::new("You are halfway there!") + text("You are halfway there!") .width(Length::Fill) .size(30) .horizontal_alignment(alignment::Horizontal::Center), ) - .push(Column::new().height(Length::Units(4096))) - .push(ferris(200, ContentFit::Contain)) + .push(vertical_space(Length::Units(4096))) + .push(ferris(300)) .push( - Text::new("You made it!") + text("You made it!") .width(Length::Fill) .size(50) .horizontal_alignment(alignment::Horizontal::Center), ) } - fn text_input( - value: &str, - is_secure: bool, - state: &'a mut text_input::State, - ) -> Column<'a, StepMessage> { - let text_input = TextInput::new( - state, + 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(Text::new( - "Use a text input to ask for different kinds of information.", - )) + .push("Use a text input to ask for different kinds of information.") .push(if is_secure { text_input.password() } else { text_input }) - .push(Checkbox::new( - is_secure, + .push(checkbox( "Enable password mode", + is_secure, StepMessage::ToggleSecureInput, )) - .push(Text::new( + .push( "A text input produces a message every time it changes. It is \ very easy to keep track of its contents:", - )) + ) .push( - Text::new(if value.is_empty() { + text(if value.is_empty() { "You have not typed anything yet..." } else { value @@ -705,79 +560,62 @@ impl<'a> Step { fn debugger(debug: bool) -> Column<'a, StepMessage> { Self::container("Debugger") - .push(Text::new( + .push( "You can ask Iced to visually explain the layouting of the \ different elements comprising your UI!", - )) - .push(Text::new( + ) + .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::new("Not available on web yet!") + text("Not available on web yet!") .style(Color::from([0.7, 0.7, 0.7])) .horizontal_alignment(alignment::Horizontal::Center), ) } else { - Element::new(Checkbox::new( - debug, - "Explain layout", - StepMessage::DebugToggled, - )) + checkbox("Explain layout", debug, StepMessage::DebugToggled) + .into() }) - .push(Text::new("Feel free to go back and take a look.")) + .push("Feel free to go back and take a look.") } fn end() -> Column<'a, StepMessage> { Self::container("You reached the end!") - .push(Text::new( - "This tour will be updated as more features are added.", - )) - .push(Text::new("Make sure to keep an eye on it!")) + .push("This tour will be updated as more features are added.") + .push("Make sure to keep an eye on it!") } } -fn ferris<'a>( - height: u16, - content_fit: ContentFit, -) -> Container<'a, StepMessage> { - Container::new( +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::new("tour/images/ferris.png") + image("tour/images/ferris.png") } else { - Image::new(format!( - "{}/images/ferris.png", - env!("CARGO_MANIFEST_DIR"), - )) + image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } - .height(Length::Units(height)) - .content_fit(content_fit), + .width(Length::Units(width)), ) .width(Length::Fill) .center_x() } -fn button<'a, Message: Clone>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - Button::new( - state, - Text::new(label).horizontal_alignment(alignment::Horizontal::Center), +fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + iced::widget::button( + text(label).horizontal_alignment(alignment::Horizontal::Center), ) .padding(12) .width(Length::Units(100)) } -fn color_slider( - state: &mut slider::State, +fn color_slider<'a>( component: f32, - update: impl Fn(f32) -> Color + 'static, -) -> Slider { - Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + update: impl Fn(f32) -> Color + 'a, +) -> Slider<'a, f64, StepMessage, Renderer> { + slider(0.0..=1.0, f64::from(component), move |c| { StepMessage::TextColorChanged(update(c as f32)) }) .step(0.01) diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 3695b6b778..3257b5190a 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,7 +1,7 @@ use iced::executor; +use iced::widget::{container, text}; use iced::{ - Application, Command, Container, Element, Length, Settings, Subscription, - Text, Theme, + Application, Command, Element, Length, Settings, Subscription, Theme, }; use iced_native::{ event::{MacOS, PlatformSpecific}, @@ -55,13 +55,13 @@ impl Application for App { iced_native::subscription::events().map(Message::EventOccurred) } - fn view(&mut self) -> Element { + fn view(&self) -> Element { let content = match &self.url { - Some(url) => Text::new(url), - None => Text::new("No URL received yet!"), + Some(url) => text(url), + None => text("No URL received yet!"), }; - Container::new(content.size(48)) + container(content.size(48)) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index 327b832468..3981f69983 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["tokio", "debug"] } iced_native = { path = "../../native" } iced_futures = { path = "../../futures" } +lazy_static = "1.4" [dependencies.async-tungstenite] version = "0.16" diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index 8832188053..ae65e064a7 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -8,6 +8,7 @@ use futures::sink::SinkExt; use futures::stream::StreamExt; use async_tungstenite::tungstenite; +use std::fmt; pub fn connect() -> Subscription { struct Connect; @@ -63,7 +64,7 @@ pub fn connect() -> Subscription { } message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(String::from(message))).await; + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; if result.is_ok() { (None, State::Connected(websocket, input)) @@ -133,14 +134,14 @@ impl Message { } } -impl From for String { - fn from(message: Message) -> Self { - match message { - Message::Connected => String::from("Connected successfully!"), +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Message::Connected => write!(f, "Connected successfully!"), Message::Disconnected => { - String::from("Connection lost... Retrying...") + write!(f, "Connection lost... Retrying...") } - Message::User(message) => message, + Message::User(message) => write!(f, "{}", message), } } } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 64addc8f83..3902e04c82 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,13 +1,12 @@ mod echo; use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; use iced::executor; -use iced::scrollable::{self, Scrollable}; -use iced::text_input::{self, TextInput}; +use iced::widget::{ + button, column, container, row, scrollable, text, text_input, Column, +}; use iced::{ - Application, Color, Column, Command, Container, Element, Length, Row, - Settings, Subscription, Text, Theme, + Application, Color, Command, Element, Length, Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -17,10 +16,7 @@ pub fn main() -> iced::Result { #[derive(Default)] struct WebSocket { messages: Vec, - message_log: scrollable::State, new_message: String, - new_message_state: text_input::State, - new_message_button: button::State, state: State, } @@ -53,45 +49,52 @@ impl Application for WebSocket { match message { Message::NewMessageChanged(new_message) => { self.new_message = new_message; + + Command::none() } Message::Send(message) => match &mut self.state { State::Connected(connection) => { self.new_message.clear(); connection.send(message); + + Command::none() } - State::Disconnected => {} + State::Disconnected => Command::none(), }, Message::Echo(event) => match event { echo::Event::Connected(connection) => { self.state = State::Connected(connection); self.messages.push(echo::Message::connected()); + + Command::none() } echo::Event::Disconnected => { self.state = State::Disconnected; self.messages.push(echo::Message::disconnected()); + + Command::none() } echo::Event::MessageReceived(message) => { self.messages.push(message); - self.message_log.snap_to(1.0); + + scrollable::snap_to(MESSAGE_LOG.clone(), 1.0) } }, - Message::Server => {} + Message::Server => Command::none(), } - - Command::none() } fn subscription(&self) -> Subscription { echo::connect().map(Message::Echo) } - fn view(&mut self) -> Element { - let message_log = if self.messages.is_empty() { - Container::new( - Text::new("Your messages will appear here...") + fn view(&self) -> Element { + let message_log: Element<_> = if self.messages.is_empty() { + container( + text("Your messages will appear here...") .style(Color::from_rgb8(0x88, 0x88, 0x88)), ) .width(Length::Fill) @@ -100,31 +103,33 @@ impl Application for WebSocket { .center_y() .into() } else { - self.messages - .iter() - .cloned() - .fold( - Scrollable::new(&mut self.message_log), - |scrollable, message| scrollable.push(Text::new(message)), + scrollable( + Column::with_children( + self.messages + .iter() + .cloned() + .map(text) + .map(Element::from) + .collect(), ) .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .into() + .spacing(10), + ) + .id(MESSAGE_LOG.clone()) + .height(Length::Fill) + .into() }; let new_message_input = { - let mut input = TextInput::new( - &mut self.new_message_state, + let mut input = text_input( "Type a message...", &self.new_message, Message::NewMessageChanged, ) .padding(10); - let mut button = Button::new( - &mut self.new_message_button, - Text::new("Send") + let mut button = button( + text("Send") .height(Length::Fill) .vertical_alignment(alignment::Vertical::Center), ) @@ -137,12 +142,10 @@ impl Application for WebSocket { } } - Row::with_children(vec![input.into(), button.into()]) - .spacing(10) - .align_items(Alignment::Fill) + row![input, button].spacing(10).align_items(Alignment::Fill) }; - Column::with_children(vec![message_log, new_message_input.into()]) + column![message_log, new_message_input] .width(Length::Fill) .height(Length::Fill) .padding(20) @@ -161,3 +164,7 @@ impl Default for State { Self::Disconnected } } + +lazy_static::lazy_static! { + static ref MESSAGE_LOG: scrollable::Id = scrollable::Id::unique(); +} diff --git a/glutin/src/application.rs b/glutin/src/application.rs index dddf006748..24f315fbfa 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -12,7 +12,7 @@ use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; use iced_winit::user_interface; -use iced_winit::{Clipboard, Debug, Proxy, Settings}; +use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; use glutin::window::Window; use std::mem::ManuallyDrop; @@ -39,9 +39,9 @@ where debug.startup_started(); let mut event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; let proxy = Proxy::new(event_loop.create_proxy()); @@ -54,8 +54,6 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); - let context = { let builder = settings.window.into_builder( &application.title(), @@ -125,18 +123,6 @@ where })? }; - let mut clipboard = Clipboard::connect(context.window()); - - application::run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - context.window(), - || compositor.fetch_information(), - ); - runtime.track(subscription); - let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::( @@ -144,11 +130,11 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, context, + init_command, settings.exit_on_close_request, )); @@ -196,11 +182,11 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, - mut clipboard: Clipboard, mut proxy: glutin::event_loop::EventLoopProxy, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver>, mut context: glutin::ContextWrapper, + init_command: Command, exit_on_close_request: bool, ) where A: Application + 'static, @@ -211,12 +197,29 @@ async fn run_instance( use glutin::event; use iced_winit::futures::stream::StreamExt; + let mut clipboard = Clipboard::connect(context.window()); + let mut cache = user_interface::Cache::default(); let mut state = application::State::new(&application, context.window()); let mut viewport_version = state.viewport_version(); + application::run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + context.window(), + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, user_interface::Cache::default(), &mut renderer, state.logical_size(), @@ -258,12 +261,15 @@ async fn run_instance( user_interface::State::Outdated ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application application::update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, @@ -280,7 +286,7 @@ async fn run_instance( user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 9750011286..49d4d9c640 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -17,7 +17,6 @@ font-source = ["font-kit"] font-fallback = [] font-icons = [] opengl = [] -pure = ["iced_pure"] [dependencies] glam = "0.10" @@ -36,11 +35,6 @@ path = "../native" version = "0.4" path = "../style" -[dependencies.iced_pure] -version = "0.2" -path = "../pure" -optional = true - [dependencies.lyon] version = "1.0" optional = true diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index a7a1cabba4..1108247208 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -36,9 +36,6 @@ pub mod triangle; pub mod widget; pub mod window; -#[doc(no_inline)] -pub use widget::*; - pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 3c19fbfb3b..cdbc4f4062 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -58,7 +58,7 @@ where element: &Element<'a, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - let layout = element.layout(self, limits); + let layout = element.as_widget().layout(self, limits); self.backend.trim_measurements(); diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index cf500a69b9..e7fab97c17 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -14,6 +14,3 @@ 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/canvas.rs b/graphics/src/widget/canvas.rs index c3e28e8cb1..88403fd72f 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -3,8 +3,6 @@ //! 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! -use crate::renderer::{self, Renderer}; -use crate::{Backend, Primitive}; pub mod event; pub mod path; @@ -29,42 +27,31 @@ pub use program::Program; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use text::Text; -use iced_native::layout; +use crate::{Backend, Primitive, Renderer}; + +use iced_native::layout::{self, Layout}; use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; use iced_native::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, - Widget, + Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; use std::marker::PhantomData; /// A widget capable of drawing 2D graphics. /// -/// # Examples -/// The repository has a couple of [examples] showcasing how to use a -/// [`Canvas`]: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`game_of_life`], an interactive version of the Game of Life, invented by -/// John Conway. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// -/// [examples]: https://github.com/iced-rs/iced/tree/0.4/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.4/examples/clock -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.4/examples/game_of_life -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.4/examples/solar_system -/// /// ## 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 use iced_graphics::canvas; +/// # pub mod widget { +/// # pub use iced_graphics::widget::canvas; +/// # } /// # pub use iced_native::{Color, Rectangle, Theme}; /// # } -/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; /// use iced::{Color, Rectangle, Theme}; /// /// // First, we define the data we need for drawing @@ -75,7 +62,9 @@ use std::marker::PhantomData; /// /// // Then, we implement the `Program` trait /// impl Program<()> for Circle { -/// fn draw(&self, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// type State = (); +/// +/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(bounds.size()); /// @@ -140,6 +129,15 @@ where P: Program, B: Backend, { + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + fn width(&self) -> Length { self.width } @@ -161,6 +159,7 @@ where fn on_event( &mut self, + tree: &mut Tree, event: iced_native::Event, layout: Layout<'_>, cursor_position: Point, @@ -183,8 +182,10 @@ where 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(canvas_event, bounds, cursor); + self.program.update(state, canvas_event, bounds, cursor); if let Some(message) = message { shell.publish(message); @@ -198,6 +199,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -205,12 +207,14 @@ where ) -> mouse::Interaction { let bounds = layout.bounds(); let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); - self.program.mouse_interaction(bounds, cursor) + self.program.mouse_interaction(state, bounds, cursor) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &T, _style: &renderer::Style, @@ -228,12 +232,13 @@ where 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(theme, bounds, cursor) + .draw(state, theme, bounds, cursor) .into_iter() .map(Geometry::into_primitive) .collect(), @@ -245,7 +250,7 @@ where impl<'a, Message, P, B, T> From> for Element<'a, Message, Renderer> where - Message: 'static, + Message: 'a, P: Program + 'a, B: Backend, T: 'a, diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs index a469417de3..5af694e9c2 100644 --- a/graphics/src/widget/canvas/cache.rs +++ b/graphics/src/widget/canvas/cache.rs @@ -1,7 +1,5 @@ -use crate::{ - canvas::{Frame, Geometry}, - Primitive, -}; +use crate::widget::canvas::{Frame, Geometry}; +use crate::Primitive; use iced_native::Size; use std::{cell::RefCell, sync::Arc}; diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 2f46079c20..417412b2c5 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -2,11 +2,10 @@ use std::borrow::Cow; use iced_native::{Point, Rectangle, Size, Vector}; -use crate::{ - canvas::path, - canvas::{Fill, Geometry, Path, Stroke, Text}, - triangle, Primitive, -}; +use crate::triangle; +use crate::widget::canvas::path; +use crate::widget::canvas::{Fill, Geometry, Path, Stroke, Text}; +use crate::Primitive; use lyon::tessellation; diff --git a/graphics/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs index 608507ad2c..aeb2589e02 100644 --- a/graphics/src/widget/canvas/path.rs +++ b/graphics/src/widget/canvas/path.rs @@ -7,7 +7,7 @@ mod builder; pub use arc::Arc; pub use builder::Builder; -use crate::canvas::LineDash; +use crate::widget::canvas::LineDash; use iced_native::{Point, Size}; use lyon::algorithms::walk::{walk_along_path, RepeatedPattern, WalkerEvent}; diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs index c49ccdc337..5121aa6861 100644 --- a/graphics/src/widget/canvas/path/builder.rs +++ b/graphics/src/widget/canvas/path/builder.rs @@ -1,4 +1,4 @@ -use crate::canvas::path::{arc, Arc, Path}; +use crate::widget::canvas::path::{arc, Arc, Path}; use iced_native::{Point, Size}; use lyon::path::builder::SvgPathBuilder; diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index dddc387d7d..656dbfa627 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -1,8 +1,7 @@ -use crate::canvas::event::{self, Event}; -use crate::canvas::{Cursor, Geometry}; - -use iced_native::mouse; -use iced_native::Rectangle; +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::{Cursor, Geometry}; +use crate::Rectangle; /// The state and logic of a [`Canvas`]. /// @@ -11,7 +10,10 @@ use iced_native::Rectangle; /// /// [`Canvas`]: crate::widget::Canvas pub trait Program { - /// Updates the state of the [`Program`]. + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. /// /// When a [`Program`] is used in a [`Canvas`], the runtime will call this /// method for each [`Event`]. @@ -23,7 +25,8 @@ pub trait Program { /// /// [`Canvas`]: crate::widget::Canvas fn update( - &mut self, + &self, + _state: &mut Self::State, _event: Event, _bounds: Rectangle, _cursor: Cursor, @@ -40,6 +43,7 @@ pub trait Program { /// [`Cache`]: crate::widget::canvas::Cache fn draw( &self, + state: &Self::State, theme: &Theme, bounds: Rectangle, cursor: Cursor, @@ -53,6 +57,7 @@ pub trait Program { /// [`Canvas`]: crate::widget::Canvas fn mouse_interaction( &self, + _state: &Self::State, _bounds: Rectangle, _cursor: Cursor, ) -> mouse::Interaction { @@ -60,33 +65,38 @@ pub trait Program { } } -impl Program for &mut T +impl Program for &T where T: Program, { + type State = T::State; + fn update( - &mut self, + &self, + state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option) { - T::update(self, event, bounds, cursor) + T::update(self, state, event, bounds, cursor) } fn draw( &self, + state: &Self::State, theme: &Theme, bounds: Rectangle, cursor: Cursor, ) -> Vec { - T::draw(self, theme, bounds, cursor) + T::draw(self, state, theme, bounds, cursor) } fn mouse_interaction( &self, + state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { - T::mouse_interaction(self, bounds, cursor) + T::mouse_interaction(self, state, bounds, cursor) } } diff --git a/graphics/src/widget/pure.rs b/graphics/src/widget/pure.rs deleted file mode 100644 index ee530379db..0000000000 --- a/graphics/src/widget/pure.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Leverage pure, virtual widgets in your application. -#[cfg(feature = "canvas")] -pub mod canvas; - -#[cfg(feature = "canvas")] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -pub use qr_code::QRCode; diff --git a/graphics/src/widget/pure/canvas.rs b/graphics/src/widget/pure/canvas.rs deleted file mode 100644 index 19bcb0de2f..0000000000 --- a/graphics/src/widget/pure/canvas.rs +++ /dev/null @@ -1,246 +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! -mod program; - -pub use crate::widget::canvas::{Canvas as _, Program as _, *}; - -pub use program::Program; - -use crate::{Backend, Primitive, Renderer}; - -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size, Vector}; -use iced_pure::widget::tree::{self, Tree}; -use iced_pure::{Element, Widget}; - -use std::marker::PhantomData; - -/// A widget capable of drawing 2D graphics. -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub mod pure { -/// # pub mod widget { -/// # pub use iced_graphics::pure::canvas; -/// # } -/// # } -/// # pub use iced_native::{Color, Rectangle, Theme}; -/// # } -/// use iced::pure::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle, Theme}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// type State = (); -/// -/// fn draw(&self, _state: &(), _theme: &Theme, 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, - message_: PhantomData, - theme_: PhantomData, -} - -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, - message_: PhantomData, - theme_: PhantomData, - } - } - - /// Sets the width of the [`Canvas`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the height of the [`Canvas`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } -} - -impl Widget> for Canvas -where - P: Program, - B: Backend, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - 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<'_, 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, - theme: &T, - _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, theme, bounds, cursor) - .into_iter() - .map(Geometry::into_primitive) - .collect(), - }); - }); - } -} - -impl<'a, Message, P, B, T> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - P: Program + 'a, - B: Backend, - T: 'a, -{ - 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 deleted file mode 100644 index 20c6406e96..0000000000 --- a/graphics/src/widget/pure/canvas/program.rs +++ /dev/null @@ -1,102 +0,0 @@ -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 internal state mutated by the [`Program`]. - type State: Default + 'static; - - /// Updates the [`State`](Self::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, - theme: &Theme, - 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 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, - theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec { - T::draw(self, state, theme, bounds, cursor) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/graphics/src/widget/pure/qr_code.rs b/graphics/src/widget/pure/qr_code.rs deleted file mode 100644 index bff391feb9..0000000000 --- a/graphics/src/widget/pure/qr_code.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! 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, T> 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, - theme: &T, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >>::draw( - self, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } -} - -impl<'a, Message, B, T> From> - for Element<'a, Message, Renderer> -where - B: Backend, -{ - fn from(qr_code: QRCode<'a>) -> Self { - Self::new(qr_code) - } -} diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs index 1a5c0b0aa8..12ce5b1f90 100644 --- a/graphics/src/widget/qr_code.rs +++ b/graphics/src/widget/qr_code.rs @@ -1,9 +1,10 @@ //! Encode and display information in a QR code. -use crate::canvas; use crate::renderer::{self, Renderer}; +use crate::widget::canvas; use crate::Backend; use iced_native::layout; +use iced_native::widget::Tree; use iced_native::{ Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; @@ -72,6 +73,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, _theme: &T, _style: &renderer::Style, diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index 9a184dd1fe..12e4e313fb 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -10,17 +10,9 @@ documentation = "https://docs.rs/iced_lazy" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] -[features] -pure = ["iced_pure"] - [dependencies] ouroboros = "0.13" [dependencies.iced_native] version = "0.5" path = "../native" - -[dependencies.iced_pure] -version = "0.2" -path = "../pure" -optional = true diff --git a/lazy/src/component.rs b/lazy/src/component.rs index eac7e8ee35..ea4d731162 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -1,17 +1,16 @@ //! Build and reuse custom widgets using The Elm Architecture. -use crate::{Cache, CacheBuilder}; - 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::widget::tree::{self, Tree}; use iced_native::{ Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; use ouroboros::self_referencing; -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::marker::PhantomData; /// A reusable, custom widget that uses The Elm Architecture. @@ -28,17 +27,24 @@ use std::marker::PhantomData; /// 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, event: Self::Event) -> Option; + 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(&mut self) -> Element<'_, Self::Event, Renderer>; + fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; } /// Turns an implementor of [`Component`] into an [`Element`] that can be @@ -48,6 +54,7 @@ pub fn view<'a, C, Message, Renderer>( ) -> Element<'a, Message, Renderer> where C: Component + 'a, + C::State: 'static, Message: 'a, Renderer: iced_native::Renderer + 'a, { @@ -56,36 +63,48 @@ where StateBuilder { component: Box::new(component), message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |_| None, - } - .build(), - ) - }, + state: PhantomData, + element_builder: |_| None, } .build(), )), }) } -struct Instance<'a, Message, Renderer, Event> { - state: RefCell>>, +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell>>, } #[self_referencing] -struct State<'a, Message: 'a, Renderer: 'a, Event: 'a> { - component: Box + 'a>, +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box + 'a>, message: PhantomData, + state: PhantomData, - #[borrows(mut component)] + #[borrows(component)] #[covariant] - cache: Option>, + element: Option>, } -impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { +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, @@ -101,34 +120,43 @@ impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { .borrow_mut() .as_mut() .unwrap() - .with_cache_mut(|cache| { - let mut element = cache.take().unwrap().into_heads().element; - let result = f(&mut element); - - *cache = Some( - CacheBuilder { - element, - overlay_builder: |_| None, - } - .build(), - ); - - result - }) + .with_element_mut(|element| f(element.as_mut().unwrap())) } } -impl<'a, Message, Renderer, Event> Widget - for Instance<'a, Message, Renderer, Event> +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 { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec { + self.rebuild_element(&S::default()); + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + self.rebuild_element(tree.state.downcast_ref()); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }) + } + fn width(&self) -> Length { - self.with_element(|element| element.width()) + self.with_element(|element| element.as_widget().width()) } fn height(&self) -> Length { - self.with_element(|element| element.height()) + self.with_element(|element| element.as_widget().height()) } fn layout( @@ -136,11 +164,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.with_element(|element| element.layout(renderer, limits)) + 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, @@ -152,7 +183,8 @@ where let mut local_shell = Shell::new(&mut local_messages); let event_status = self.with_element_mut(|element| { - element.on_event( + element.as_widget_mut().on_event( + &mut tree.children[0], event, layout, cursor_position, @@ -165,37 +197,31 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { - let mut component = self - .state - .borrow_mut() - .take() - .unwrap() - .into_heads() - .component; - - for message in local_messages - .into_iter() - .filter_map(|message| component.update(message)) - { + 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.borrow_mut() = Some( + self.state = RefCell::new(Some( StateBuilder { - component, + component: heads.component, message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |_| None, - } - .build(), - ) + 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)) + }); shell.invalidate_layout(); } @@ -205,6 +231,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -213,7 +240,8 @@ where viewport: &Rectangle, ) { self.with_element(|element| { - element.draw( + element.as_widget().draw( + &tree.children[0], renderer, theme, style, @@ -226,13 +254,15 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_element(|element| { - element.mouse_interaction( + element.as_widget().mouse_interaction( + &tree.children[0], layout, cursor_position, viewport, @@ -241,63 +271,72 @@ where }) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { - let has_overlay = self - .state - .borrow_mut() - .as_mut() - .unwrap() - .with_cache_mut(|cache| { - let element = cache.take().unwrap().into_heads().element; - - *cache = Some( - CacheBuilder { - element, - overlay_builder: |element| { - element.overlay(layout, renderer) - }, - } - .build(), - ); - - cache + ) -> 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_overlay() + .borrow_element() .as_ref() - .map(|overlay| overlay.position()) - }); + .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(Overlay { instance: self }), + Box::new(OverlayInstance { + overlay: Some(overlay), + }), ) }) } } -struct Overlay<'a, 'b, Message, Renderer, Event> { - instance: &'b mut Instance<'a, Message, Renderer, Event>, +#[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> - Overlay<'a, 'b, Message, Renderer, Event> +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.instance - .state - .borrow() - .as_ref() - .unwrap() - .borrow_cache() + self.overlay .as_ref() .unwrap() .borrow_overlay() @@ -306,27 +345,21 @@ impl<'a, 'b, Message, Renderer, Event> } fn with_overlay_mut_maybe( - &self, + &mut self, f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, ) -> Option { - self.instance - .state - .borrow_mut() + self.overlay .as_mut() .unwrap() - .with_cache_mut(|cache| { - cache - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - }) + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } -impl<'a, 'b, Message, Renderer, Event> overlay::Overlay - for Overlay<'a, 'b, Message, Renderer, Event> +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, @@ -401,32 +434,43 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { - let mut component = - self.instance.state.take().unwrap().into_heads().component; - - for message in local_messages - .into_iter() - .filter_map(|message| component.update(message)) - { + 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); } - self.instance.state = RefCell::new(Some( + *overlay.instance.state.borrow_mut() = Some( StateBuilder { - component, + component: heads.component, message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |_| None, - } - .build(), - ) + state: PhantomData, + element_builder: |state| { + Some(state.view(overlay.tree.state.downcast_ref::())) }, } .build(), - )); + ); + + overlay.instance.with_element(|element| { + overlay.tree.diff_children(std::slice::from_ref(&element)) + }); + + self.overlay = Some( + OverlayBuilder { + instance: overlay.instance, + instance_ref_builder: |instance| instance.state.borrow(), + tree: overlay.tree, + types: PhantomData, + overlay_builder: |_, _| None, + } + .build(), + ); shell.invalidate_layout(); } diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index aed11e9f52..3827746c05 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -20,13 +20,32 @@ pub mod component; pub mod responsive; -#[cfg(feature = "pure")] -#[cfg_attr(docsrs, doc(cfg(feature = "pure")))] -pub mod pure; - pub use component::Component; pub use responsive::Responsive; mod cache; -use cache::{Cache, CacheBuilder}; +use iced_native::{Element, Size}; + +/// 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.rs b/lazy/src/pure.rs deleted file mode 100644 index dc500e5ee2..0000000000 --- a/lazy/src/pure.rs +++ /dev/null @@ -1,31 +0,0 @@ -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/component.rs b/lazy/src/pure/component.rs deleted file mode 100644 index b414a14992..0000000000 --- a/lazy/src/pure/component.rs +++ /dev/null @@ -1,479 +0,0 @@ -//! Build and reuse custom widgets using The Elm Architecture. -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size}; -use iced_pure::widget::tree::{self, Tree}; -use iced_pure::{Element, Widget}; - -use ouroboros::self_referencing; -use std::cell::{Ref, RefCell}; -use std::marker::PhantomData; - -/// A reusable, custom widget that uses The Elm Architecture. -/// -/// A [`Component`] allows you to implement custom widgets as if they were -/// `iced` applications with encapsulated state. -/// -/// In other words, a [`Component`] allows you to turn `iced` applications into -/// custom widgets and embed them without cumbersome wiring. -/// -/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) -/// and update the internal state of the [`Component`]. -/// -/// Additionally, a [`Component`] is capable of producing a `Message` to notify -/// the parent application of any relevant interactions. -pub trait Component { - /// 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<'_, Self::Event, Renderer>; -} - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn view<'a, C, Message, Renderer>( - component: C, -) -> Element<'a, Message, Renderer> -where - C: Component + '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 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, - ) -> T { - self.with_element_mut(|element| f(element)) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, - ) -> T { - self.state - .borrow_mut() - .as_mut() - .unwrap() - .with_element_mut(|element| f(element.as_mut().unwrap())) - } -} - -impl<'a, Message, Renderer, Event, S> Widget - for Instance<'a, Message, Renderer, Event, S> -where - S: 'static + Default, - Renderer: iced_native::Renderer, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(S::default()) - } - - fn children(&self) -> Vec { - self.rebuild_element(&S::default()); - self.with_element(|element| vec![Tree::new(element)]) - } - - fn diff(&self, tree: &mut Tree) { - self.rebuild_element(tree.state.downcast_ref()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }) - } - - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.with_element(|element| { - element.as_widget().layout(renderer, limits) - }) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self.with_element_mut(|element| { - element.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }); - - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if !local_messages.is_empty() { - let mut heads = self.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(tree.state.downcast_mut::(), 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(), - )); - - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }); - - shell.invalidate_layout(); - } - - event_status - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.with_element(|element| { - element.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - }); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }) - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> iced_native::event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self - .with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }) - .unwrap_or(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(), - ); - - overlay.instance.with_element(|element| { - overlay.tree.diff_children(std::slice::from_ref(&element)) - }); - - self.overlay = Some( - OverlayBuilder { - instance: overlay.instance, - instance_ref_builder: |instance| instance.state.borrow(), - tree: overlay.tree, - types: PhantomData, - overlay_builder: |_, _| None, - } - .build(), - ); - - shell.invalidate_layout(); - } - - event_status - } -} diff --git a/lazy/src/pure/responsive.rs b/lazy/src/pure/responsive.rs deleted file mode 100644 index 0964ebc83d..0000000000 --- a/lazy/src/pure/responsive.rs +++ /dev/null @@ -1,388 +0,0 @@ -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Size}; -use iced_pure::horizontal_space; -use iced_pure::overlay; -use iced_pure::widget::tree::{self, Tree}; -use iced_pure::{Element, Widget}; - -use ouroboros::self_referencing; -use std::cell::{RefCell, RefMut}; -use std::marker::PhantomData; -use std::ops::Deref; - -/// A widget that is aware of its dimensions. -/// -/// A [`Responsive`] widget will always try to fill all the available space of -/// its parent. -#[allow(missing_debug_implementations)] -pub struct Responsive<'a, Message, Renderer> { - view: Box 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> -where - Renderer: iced_native::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; - - tree.diff(&self.element); - - self.layout = self - .element - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); - } - - 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, - theme: &Renderer::Theme, - 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, - theme, - 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_mut(), - tree: state.tree.borrow_mut(), - types: PhantomData, - overlay_builder: |content, tree| { - content.update( - tree, - renderer, - layout.bounds().size(), - &self.view, - ); - - let content_layout = Layout::with_offset( - layout.position() - Point::ORIGIN, - &content.layout, - ); - - content.element.as_widget().overlay( - tree, - content_layout, - renderer, - ) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); - - has_overlay - .map(|position| overlay::Element::new(position, Box::new(overlay))) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - Message: 'a, -{ - fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { - Self::new(responsive) - } -} - -#[self_referencing] -struct Overlay<'a, 'b, Message, Renderer> { - content: RefMut<'a, Content<'b, Message, Renderer>>, - tree: RefMut<'a, Tree>, - types: PhantomData, - - #[borrows(mut 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, 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(iced_native::event::Status::Ignored) - } -} diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 4a3eb5c6ac..0b7ae6de10 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -1,71 +1,131 @@ -//! Build responsive widgets. -use crate::{Cache, CacheBuilder}; - -use iced_native::event::{self, Event}; +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::window; +use iced_native::widget::horizontal_space; +use iced_native::widget::tree::{self, Tree}; use iced_native::{ Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; -use std::cell::RefCell; +use ouroboros::self_referencing; +use std::cell::{RefCell, RefMut}; +use std::marker::PhantomData; use std::ops::Deref; -/// The state of a [`Responsive`] widget. -#[derive(Debug, Clone, Default)] -pub struct State { - last_size: Option, - last_layout: layout::Node, -} - -impl State { - pub fn new() -> State { - State::default() - } - - fn layout(&self, parent: Layout<'_>) -> Layout<'_> { - Layout::with_offset( - parent.position() - Point::ORIGIN, - &self.last_layout, - ) - } -} - /// 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>( - RefCell>, -); +pub struct Responsive<'a, Message, Renderer> { + view: Box Element<'a, Message, Renderer> + 'a>, + content: RefCell>, +} -impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> { - /// Creates a new [`Responsive`] widget with the given [`State`] and a - /// closure that produces its contents. +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( - state: &'a mut State, - view: impl FnOnce(Size) -> Element<'a, Message, Renderer> + 'a, + view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, ) -> Self { - Self(RefCell::new(Internal { - state, - content: Content::Pending(Some(Box::new(view))), - })) + 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> +where + Renderer: iced_native::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; + + tree.diff(&self.element); + + self.layout = self + .element + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); + } + + 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 } @@ -79,45 +139,44 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.max(); - - self.0.borrow_mut().state.last_size = Some(size); - - layout::Node::new(size) + layout::Node::new(limits.max()) } fn on_event( &mut self, - event: Event, + 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 internal = self.0.borrow_mut(); - - if matches!(event, Event::Window(window::Event::Resized { .. })) - || internal.state.last_size - != Some(internal.state.last_layout.size()) - { - shell.invalidate_widgets(); - } - - internal.resolve(renderer, |state, renderer, content| { - content.on_event( - event, - state.layout(layout), - cursor_position, - renderer, - clipboard, - shell, - ) - }) + 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, theme: &Renderer::Theme, style: &renderer::Style, @@ -125,168 +184,96 @@ where cursor_position: Point, viewport: &Rectangle, ) { - let mut internal = self.0.borrow_mut(); - - internal.resolve(renderer, |state, renderer, content| { - content.draw( - renderer, - theme, - style, - state.layout(layout), - cursor_position, - viewport, - ) - }) + 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, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) } fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let mut internal = self.0.borrow_mut(); - - internal.resolve(renderer, |state, renderer, content| { - content.mouse_interaction( - state.layout(layout), - cursor_position, - viewport, - renderer, - ) - }) + 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( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { - let has_overlay = { - use std::ops::DerefMut; - - let mut internal = self.0.borrow_mut(); - - let _ = - internal.resolve(renderer, |_state, _renderer, _content| {}); - - let Internal { content, state } = internal.deref_mut(); - - let content_layout = state.layout(layout); - - match content { - Content::Pending(_) => None, - Content::Ready(cache) => { - *cache = Some( - CacheBuilder { - element: cache.take().unwrap().into_heads().element, - overlay_builder: |element| { - element.overlay(content_layout, renderer) - }, - } - .build(), - ); - - cache - .as_ref() - .unwrap() - .borrow_overlay() - .as_ref() - .map(|overlay| overlay.position()) - } - } - }; - - has_overlay.map(|position| { - overlay::Element::new( - position, - Box::new(Overlay { instance: self }), - ) - }) - } -} - -struct Internal<'a, Message, Renderer> { - state: &'a mut State, - content: Content<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Internal<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn resolve( - &mut self, - renderer: R, - f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, - ) -> T - where - R: Deref, - { - self.content.resolve(self.state, renderer, f) - } -} - -enum Content<'a, Message, Renderer> { - Pending( - Option Element<'a, Message, Renderer> + 'a>>, - ), - Ready(Option>), -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn resolve( - &mut self, - state: &mut State, - renderer: R, - f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, - ) -> T - where - R: Deref, - { - match self { - Content::Ready(cache) => { - let mut heads = cache.take().unwrap().into_heads(); - - let result = f(state, renderer, &mut heads.element); - - *cache = Some( - CacheBuilder { - element: heads.element, - overlay_builder: |_| None, - } - .build(), + ) -> Option> { + let state = tree.state.downcast_ref::(); + + let overlay = OverlayBuilder { + content: self.content.borrow_mut(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content, tree| { + content.update( + tree, + renderer, + layout.bounds().size(), + &self.view, ); - result - } - Content::Pending(view) => { - let element = - view.take().unwrap()(state.last_size.unwrap_or(Size::ZERO)); - - state.last_layout = element.layout( - renderer.deref(), - &layout::Limits::new( - Size::ZERO, - state.last_size.unwrap_or(Size::ZERO), - ), + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &content.layout, ); - *self = Content::Ready(Some( - CacheBuilder { - element, - overlay_builder: |_| None, - } - .build(), - )); - - self.resolve(state, renderer, f) - } + content.element.as_widget().overlay( + tree, + content_layout, + renderer, + ) + }, } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) } } @@ -301,8 +288,15 @@ where } } +#[self_referencing] struct Overlay<'a, 'b, Message, Renderer> { - instance: &'b mut Responsive<'a, Message, Renderer>, + content: RefMut<'a, Content<'b, Message, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData, + + #[borrows(mut content, mut tree)] + #[covariant] + overlay: Option>, } impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { @@ -310,29 +304,14 @@ impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { &self, f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, ) -> Option { - let internal = self.instance.0.borrow(); - - match &internal.content { - Content::Pending(_) => None, - Content::Ready(cache) => { - cache.as_ref().unwrap().borrow_overlay().as_ref().map(f) - } - } + self.borrow_overlay().as_ref().map(f) } fn with_overlay_mut_maybe( - &self, + &mut self, f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, ) -> Option { - let mut internal = self.instance.0.borrow_mut(); - - match &mut internal.content { - Content::Pending(_) => None, - Content::Ready(cache) => cache - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)), - } + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } @@ -394,7 +373,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> iced_native::event::Status { + ) -> event::Status { self.with_overlay_mut_maybe(|overlay| { overlay.on_event( event, diff --git a/native/src/command.rs b/native/src/command.rs index 89d0f04576..b0b12805c7 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -3,6 +3,8 @@ mod action; pub use action::Action; +use crate::widget; + use iced_futures::MaybeSend; use std::fmt; @@ -24,6 +26,13 @@ impl Command { Self(iced_futures::Command::single(action)) } + /// Creates a [`Command`] that performs a [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation + 'static) -> Self { + Self(iced_futures::Command::single(Action::Widget( + widget::Action::new(operation), + ))) + } + /// Creates a [`Command`] that performs the action of the given future. pub fn perform( future: impl Future + 'static + MaybeSend, @@ -51,6 +60,7 @@ impl Command { ) -> Command where T: 'static, + A: 'static, { let Command(command) = self; diff --git a/native/src/command/action.rs b/native/src/command/action.rs index 1bb03cef51..3fb02899b7 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,5 +1,6 @@ use crate::clipboard; use crate::system; +use crate::widget; use crate::window; use iced_futures::MaybeSend; @@ -23,6 +24,9 @@ pub enum Action { /// Run a system action. System(system::Action), + + /// Run a widget action. + Widget(widget::Action), } impl Action { @@ -34,6 +38,7 @@ impl Action { f: impl Fn(T) -> A + 'static + MaybeSend + Sync, ) -> Action where + A: 'static, T: 'static, { use iced_futures::futures::FutureExt; @@ -43,6 +48,7 @@ impl Action { Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(window) => Action::Window(window), Self::System(system) => Action::System(system.map(f)), + Self::Widget(widget) => Action::Widget(widget.map(f)), } } } @@ -56,6 +62,7 @@ impl fmt::Debug for Action { } Self::Window(action) => write!(f, "Action::Window({:?})", action), Self::System(action) => write!(f, "Action::System({:?})", action), + Self::Widget(_action) => write!(f, "Action::Widget"), } } } diff --git a/native/src/element.rs b/native/src/element.rs index 425bddc2ef..8b994d7331 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -3,9 +3,11 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{ - Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, -}; +use crate::widget; +use crate::widget::tree::{self, Tree}; +use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell, Widget}; + +use std::borrow::Borrow; /// A generic [`Widget`]. /// @@ -15,25 +17,33 @@ use crate::{ /// If you have a [built-in widget], you should be able to use `Into` /// to turn it into an [`Element`]. /// -/// [built-in widget]: widget/index.html#built-in-widgets +/// [built-in widget]: crate::widget #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { - pub(crate) widget: Box + 'a>, + widget: Box + 'a>, } -impl<'a, Message, Renderer> Element<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new( - widget: impl Widget + 'a, - ) -> Element<'a, Message, Renderer> { - Element { + pub fn new(widget: impl Widget + 'a) -> Self + where + Renderer: crate::Renderer, + { + Self { widget: Box::new(widget), } } + /// Returns a reference to the [`Widget`] of the [`Element`], + pub fn as_widget(&self) -> &dyn Widget { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget { + self.widget.as_mut() + } + /// Applies a transformation to the produced message of the [`Element`]. /// /// This method is useful when you want to decouple different parts of your @@ -168,127 +178,22 @@ where /// } /// } /// ``` - pub fn map(self, f: F) -> Element<'a, B, Renderer> - where - Message: 'static, - Renderer: 'a, - B: 'static, - F: 'static + Fn(Message) -> B, - { - Element { - widget: Box::new(Map::new(self.widget, f)), - } - } - - /// Marks the [`Element`] as _to-be-explained_. - /// - /// The [`Renderer`] will explain the layout of the [`Element`] graphically. - /// This can be very useful for debugging your layout! - /// - /// [`Renderer`]: crate::Renderer - pub fn explain>( + pub fn map( self, - color: C, - ) -> Element<'a, Message, Renderer> + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> where - Message: 'static, - Renderer: 'a, + Message: 'a, + Renderer: crate::Renderer + 'a, + B: 'a, { - Element { - widget: Box::new(Explain::new(self, color.into())), - } - } - - /// Returns the width of the [`Element`]. - pub fn width(&self) -> Length { - self.widget.width() - } - - /// Returns the height of the [`Element`]. - pub fn height(&self) -> Length { - self.widget.height() - } - - /// Computes the layout of the [`Element`] in the given [`Limits`]. - /// - /// [`Limits`]: layout::Limits - pub fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - /// Processes a runtime [`Event`]. - pub 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.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.widget.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - /// Returns the overlay of the [`Element`], if there is any. - pub fn overlay<'b>( - &'b mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.widget.overlay(layout, renderer) + Element::new(Map::new(self.widget, f)) } } struct Map<'a, A, B, Renderer> { widget: Box + 'a>, - mapper: Box B>, + mapper: Box B + 'a>, } impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { @@ -297,7 +202,7 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { mapper: F, ) -> Map<'a, A, B, Renderer> where - F: 'static + Fn(A) -> B, + F: 'a + Fn(A) -> B, { Map { widget, @@ -309,9 +214,25 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget for Map<'a, A, B, Renderer> where Renderer: crate::Renderer + 'a, - A: 'static, - B: 'static, + A: 'a, + B: 'a, { + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + fn width(&self) -> Length { self.widget.width() } @@ -328,8 +249,45 @@ where self.widget.layout(renderer, limits) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + } + + self.widget + .operate(tree, layout, &mut MapOperation { operation }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -341,6 +299,7 @@ where let mut local_shell = Shell::new(&mut local_messages); let status = self.widget.on_event( + tree, event, layout, cursor_position, @@ -356,6 +315,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -364,6 +324,7 @@ where viewport: &Rectangle, ) { self.widget.draw( + tree, renderer, theme, style, @@ -375,12 +336,14 @@ where 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, @@ -388,134 +351,32 @@ where ) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { let mapper = &self.mapper; self.widget - .overlay(layout, renderer) + .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } } -struct Explain<'a, Message, Renderer: crate::Renderer> { - element: Element<'a, Message, Renderer>, - color: Color, -} - -impl<'a, Message, Renderer> Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, +impl<'a, Message, Renderer> Borrow + 'a> + for Element<'a, Message, Renderer> { - fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { - Explain { element, color } + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() } } -impl<'a, Message, Renderer> Widget - for Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, +impl<'a, Message, Renderer> Borrow + 'a> + for &Element<'a, Message, Renderer> { - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.element.widget.layout(renderer, limits) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.element.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - fn explain_layout( - renderer: &mut Renderer, - color: Color, - layout: Layout<'_>, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border_color: color, - border_width: 1.0, - border_radius: 0.0, - }, - Color::TRANSPARENT, - ); - - for child in layout.children() { - explain_layout(renderer, color, child); - } - } - - self.element.widget.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - - explain_layout(renderer, self.color, layout); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.widget.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.element.overlay(layout, renderer) + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() } } diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 5fbcbca0ce..94121d7690 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,8 +16,10 @@ // 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 crate::layout::{Limits, Node}; -use crate::{Alignment, Element, Padding, Point, Size}; +use crate::{Alignment, Padding, Point, Size}; /// The main axis of a flex layout. #[derive(Debug)] @@ -84,8 +86,8 @@ where items.iter().for_each(|child| { let cross_fill_factor = match axis { - Axis::Horizontal => child.height(), - Axis::Vertical => child.width(), + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), } .fill_factor(); @@ -95,7 +97,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -107,8 +109,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -130,7 +132,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -149,8 +151,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -179,7 +181,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); if align_items != Alignment::Fill { cross = cross.max(axis.cross(layout.size())); diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 792d29058a..905d3389d9 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -10,6 +10,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// An interactive component that can be displayed on top of other widgets. @@ -40,6 +42,36 @@ where cursor_position: Point, ); + /// Returns the [`Tag`] of the [`Widget`]. + /// + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _layout: Layout<'_>, + _operation: &mut dyn widget::Operation, + ) { + } + /// Processes a runtime [`Event`]. /// /// It receives: @@ -77,3 +109,26 @@ where mouse::Interaction::Idle } } + +/// Obtains the first overlay [`Element`] found in the given children. +/// +/// This method will generally only be used by advanced users that are +/// implementing the [`Widget`](crate::Widget) trait. +pub fn from_children<'a, Message, Renderer>( + children: &'a [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option> +where + Renderer: crate::Renderer, +{ + children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget().overlay(state, layout, renderer) + }) + .next() +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index de2e1f3718..b919c221ee 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; /// A generic [`Overlay`]. @@ -102,6 +103,15 @@ where self.overlay .draw(renderer, theme, style, layout, cursor_position) } + + /// Applies an [`Operation`] to the [`Element`]. + pub fn operate( + &self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, + ) { + self.overlay.operate(layout, operation); + } } struct Map<'a, A, B, Renderer> { diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 7b8d4d9e6c..0813587288 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -9,6 +9,7 @@ use crate::text::{self, Text}; use crate::touch; use crate::widget::container::{self, Container}; use crate::widget::scrollable::{self, Scrollable}; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -114,15 +115,23 @@ where } /// The local state of a [`Menu`]. -#[derive(Debug, Clone, Default)] +#[derive(Debug)] pub struct State { - scrollable: scrollable::State, + tree: Tree, } impl State { /// Creates a new [`State`] for a [`Menu`]. pub fn new() -> Self { - Self::default() + Self { + tree: Tree::empty(), + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new() } } @@ -131,6 +140,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { + state: &'a mut Tree, container: Container<'a, Message, Renderer>, width: u16, target_height: f32, @@ -161,18 +171,20 @@ where style, } = menu; - let container = - Container::new(Scrollable::new(&mut state.scrollable).push(List { - options, - hovered_option, - last_selection, - font, - text_size, - padding, - style, - })); + let container = Container::new(Scrollable::new(List { + options, + hovered_option, + last_selection, + font, + text_size, + padding, + style, + })); + + state.tree.diff(&container as &dyn Widget<_, _>); Self { + state: &mut state.tree, container, width, target_height, @@ -187,6 +199,18 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { + fn tag(&self) -> tree::Tag { + self.container.tag() + } + + fn state(&self) -> tree::State { + self.container.state() + } + + fn children(&self) -> Vec { + self.container.children() + } + fn layout( &self, renderer: &Renderer, @@ -230,6 +254,7 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { self.container.on_event( + self.state, event, layout, cursor_position, @@ -247,6 +272,7 @@ where renderer: &Renderer, ) -> mouse::Interaction { self.container.mouse_interaction( + self.state, layout, cursor_position, viewport, @@ -279,6 +305,7 @@ where ); self.container.draw( + self.state, renderer, theme, style, @@ -344,6 +371,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -407,6 +435,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -423,6 +452,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/program.rs b/native/src/program.rs index 9ee727032c..c71c237fc1 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -26,5 +26,5 @@ pub trait Program: Sized { /// Returns the widgets to display in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; + fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; } diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a7305a55cb..ef64ac36f4 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -21,7 +21,7 @@ pub trait Renderer: Sized { element: &Element<'a, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - element.layout(self, limits) + element.as_widget().layout(self, limits) } /// Draws the primitives recorded in the given closure in a new layer. diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index ef6f437e4e..4bcf1e0c5a 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -22,6 +23,7 @@ use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, base: layout::Node, + state: widget::Tree, overlay: Option, bounds: Size, } @@ -88,17 +90,21 @@ where pub fn build>>( root: E, bounds: Size, - _cache: Cache, + cache: Cache, renderer: &mut Renderer, ) -> Self { let root = root.into(); + let Cache { mut state } = cache; + state.diff(root.as_widget()); + let base = renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); UserInterface { root, base, + state, overlay: None, bounds, } @@ -182,9 +188,12 @@ where use std::mem::ManuallyDrop; let mut state = State::Updated; - let mut manual_overlay = ManuallyDrop::new( - self.root.overlay(Layout::new(&self.base), renderer), - ); + let mut manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); let (base_cursor, overlay_statuses) = if manual_overlay.is_some() { let bounds = self.bounds; @@ -215,9 +224,12 @@ where &layout::Limits::new(Size::ZERO, self.bounds), ); - manual_overlay = ManuallyDrop::new( - self.root.overlay(Layout::new(&self.base), renderer), - ); + manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); if manual_overlay.is_none() { break; @@ -262,7 +274,8 @@ where let mut shell = Shell::new(messages); - let event_status = self.root.widget.on_event( + let event_status = self.root.as_widget_mut().on_event( + &mut self.state, event, Layout::new(&self.base), base_cursor, @@ -377,9 +390,11 @@ where let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = - self.root.overlay(Layout::new(&self.base), renderer) - { + let base_cursor = if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { let overlay_layout = self .overlay .take() @@ -399,7 +414,8 @@ where cursor_position }; - self.root.widget.draw( + self.root.as_widget().draw( + &self.state, renderer, theme, style, @@ -408,7 +424,8 @@ where &viewport, ); - let base_interaction = self.root.widget.mouse_interaction( + let base_interaction = self.root.as_widget().mouse_interaction( + &self.state, Layout::new(&self.base), cursor_position, &viewport, @@ -430,52 +447,79 @@ where overlay .as_ref() .and_then(|layout| { - root.overlay(Layout::new(base), renderer).map(|overlay| { - let overlay_interaction = overlay.mouse_interaction( - Layout::new(layout), - cursor_position, - &viewport, - renderer, - ); - - let overlay_bounds = layout.bounds(); - - renderer.with_layer(overlay_bounds, |renderer| { - overlay.draw( - renderer, - theme, - style, + root.as_widget() + .overlay(&mut self.state, Layout::new(base), renderer) + .map(|overlay| { + let overlay_interaction = overlay.mouse_interaction( Layout::new(layout), cursor_position, + &viewport, + renderer, ); - }); - if overlay_bounds.contains(cursor_position) { - overlay_interaction - } else { - base_interaction - } - }) + let overlay_bounds = layout.bounds(); + + renderer.with_layer(overlay_bounds, |renderer| { + overlay.draw( + renderer, + theme, + style, + Layout::new(layout), + cursor_position, + ); + }); + + if overlay_bounds.contains(cursor_position) { + overlay_interaction + } else { + base_interaction + } + }) }) .unwrap_or(base_interaction) } + /// Applies a [`widget::Operation`] to the [`UserInterface`]. + pub fn operate( + &mut self, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.root.as_widget().operate( + &mut self.state, + Layout::new(&self.base), + operation, + ); + + if let Some(layout) = self.overlay.as_ref() { + if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + overlay.operate(Layout::new(layout), operation); + } + } + } + /// Relayouts and returns a new [`UserInterface`] using the provided /// bounds. pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { - Self::build(self.root, bounds, Cache, renderer) + Self::build(self.root, bounds, Cache { state: self.state }, renderer) } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. pub fn into_cache(self) -> Cache { - Cache + Cache { state: self.state } } } /// Reusable data of a specific [`UserInterface`]. -#[derive(Debug, Clone)] -pub struct Cache; +#[derive(Debug)] +pub struct Cache { + state: widget::Tree, +} impl Cache { /// Creates an empty [`Cache`]. @@ -483,7 +527,9 @@ impl Cache { /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. pub fn new() -> Cache { - Cache + Cache { + state: widget::Tree::empty(), + } } } diff --git a/native/src/widget.rs b/native/src/widget.rs index 9fe96e337d..8890b8e798 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -15,7 +15,9 @@ pub mod button; pub mod checkbox; pub mod column; pub mod container; +pub mod helpers; pub mod image; +pub mod operation; pub mod pane_grid; pub mod pick_list; pub mod progress_bar; @@ -30,6 +32,10 @@ pub mod text; pub mod text_input; pub mod toggler; pub mod tooltip; +pub mod tree; + +mod action; +mod id; #[doc(no_inline)] pub use button::Button; @@ -40,6 +46,8 @@ pub use column::Column; #[doc(no_inline)] pub use container::Container; #[doc(no_inline)] +pub use helpers::*; +#[doc(no_inline)] pub use image::Image; #[doc(no_inline)] pub use pane_grid::PaneGrid; @@ -69,6 +77,12 @@ pub use text_input::TextInput; pub use toggler::Toggler; #[doc(no_inline)] pub use tooltip::Tooltip; +#[doc(no_inline)] +pub use tree::Tree; + +pub use action::Action; +pub use id::Id; +pub use operation::Operation; use crate::event::{self, Event}; use crate::layout; @@ -109,12 +123,10 @@ where /// Returns the height of the [`Widget`]. fn height(&self) -> Length; - /// Returns the [`Node`] of the [`Widget`]. + /// Returns the [`layout::Node`] of the [`Widget`]. /// - /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. - /// - /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -124,6 +136,7 @@ where /// Draws the [`Widget`] using the associated `Renderer`. fn draw( &self, + state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -132,20 +145,43 @@ where viewport: &Rectangle, ); - /// Processes a runtime [`Event`]. + /// Returns the [`Tag`] of the [`Widget`]. /// - /// It receives: - /// * an [`Event`] describing user interaction - /// * the computed [`Layout`] of the [`Widget`] - /// * the current cursor position - /// * a mutable `Message` list, allowing the [`Widget`] to produce - /// new messages based on user interaction. - /// * the `Renderer` - /// * a [`Clipboard`], if available + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _operation: &mut dyn Operation, + ) { + } + + /// Processes a runtime [`Event`]. /// /// By default, it does nothing. fn on_event( &mut self, + _state: &mut Tree, _event: Event, _layout: Layout<'_>, _cursor_position: Point, @@ -161,6 +197,7 @@ where /// By default, it returns [`mouse::Interaction::Idle`]. fn mouse_interaction( &self, + _state: &Tree, _layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, @@ -170,11 +207,12 @@ where } /// Returns the overlay of the [`Widget`], if there is any. - fn overlay( - &mut self, + fn overlay<'a>( + &'a self, + _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - ) -> Option> { + ) -> Option> { None } } diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs new file mode 100644 index 0000000000..766e902b70 --- /dev/null +++ b/native/src/widget/action.rs @@ -0,0 +1,88 @@ +use crate::widget::operation::{self, Operation}; +use crate::widget::Id; + +use iced_futures::MaybeSend; + +/// An operation to be performed on the widget tree. +#[allow(missing_debug_implementations)] +pub struct Action(Box>); + +impl Action { + /// Creates a new [`Action`] with the given [`Operation`]. + pub fn new(operation: impl Operation + 'static) -> Self { + Self(Box::new(operation)) + } + + /// Maps the output of an [`Action`] using the given function. + pub fn map( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action + where + T: 'static, + A: 'static, + { + Action(Box::new(Map { + operation: self.0, + f: Box::new(f), + })) + } + + /// Consumes the [`Action`] and returns the internal [`Operation`]. + pub fn into_operation(self) -> Box> { + self.0 + } +} + +#[allow(missing_debug_implementations)] +struct Map { + operation: Box>, + f: Box B>, +} + +impl Operation for Map +where + A: 'static, + B: 'static, +{ + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + struct MapRef<'a, A, B> { + operation: &'a mut dyn Operation, + f: &'a dyn Fn(A) -> B, + } + + impl<'a, A, B> Operation for MapRef<'a, A, B> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + let Self { operation, f } = self; + + operation.container(id, &mut |operation| { + operate_on_children(&mut MapRef { operation, f }); + }); + } + } + + let Self { operation, f } = self; + + MapRef { + operation: operation.as_mut(), + f, + } + .container(id, operate_on_children); + } + + fn focusable( + &mut self, + state: &mut dyn operation::Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index a33ee7f75d..6c0b8f6e79 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -7,6 +7,8 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; +use crate::widget::Operation; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, @@ -17,8 +19,6 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -27,17 +27,13 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// ButtonPressed, /// } /// -/// let mut state = button::State::new(); -/// let button = Button::new(&mut state, Text::new("Press me!")) -/// .on_press(Message::ButtonPressed); +/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -46,12 +42,12 @@ pub use iced_style::button::{Appearance, StyleSheet}; /// ButtonPressed, /// } /// -/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { -/// Button::new(state, Text::new("I'm disabled!")) +/// fn disabled_button<'a>() -> Button<'a, Message> { +/// Button::new("I'm disabled!") /// } /// -/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { -/// disabled_button(state).on_press(Message::ButtonPressed) +/// fn enabled_button<'a>() -> Button<'a, Message> { +/// disabled_button().on_press(Message::ButtonPressed) /// } /// ``` #[allow(missing_debug_implementations)] @@ -60,7 +56,6 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, content: Element<'a, Message, Renderer>, on_press: Option, width: Length, @@ -71,24 +66,18 @@ where impl<'a, Message, Renderer> Button<'a, Message, Renderer> where - Message: Clone, Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - /// Creates a new [`Button`] with some local [`State`] and the given - /// content. - pub fn new(state: &'a mut State, content: E) -> Self - where - E: Into>, - { + /// Creates a new [`Button`] with the given content. + pub fn new(content: impl Into>) -> Self { Button { - state, content: content.into(), on_press: None, width: Length::Shrink, height: Length::Shrink, padding: Padding::new(5), - style: Default::default(), + style: ::Style::default(), } } @@ -111,13 +100,14 @@ where } /// Sets the message that will be produced when the [`Button`] is pressed. - /// If on_press isn't set, button will be disabled. + /// + /// 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 this [`Button`]. + /// Sets the style variant of this [`Button`]. pub fn style( mut self, style: ::Style, @@ -127,6 +117,174 @@ where } } +impl<'a, Message, Renderer> Widget + for Button<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::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 width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || tree.state.downcast_mut::(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + + let styling = draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + theme, + self.style, + || tree.state.downcast_ref::(), + ); + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &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 { + mouse_interaction(layout, cursor_position, self.on_press.is_some()) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: crate::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(button: Button<'a, Message, Renderer>) -> Self { + Self::new(button) + } +} + /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -292,131 +450,3 @@ pub fn mouse_interaction( mouse::Interaction::default() } } - -impl<'a, Message, Renderer> Widget - for Button<'a, Message, Renderer> -where - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |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 { - if let event::Status::Captured = self.content.on_event( - event.clone(), - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) { - return event::Status::Captured; - } - - update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || &mut self.state, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - - let styling = draw( - renderer, - bounds, - cursor_position, - self.on_press.is_some(), - theme, - self.style, - || self.state, - ); - - self.content.draw( - renderer, - theme, - &renderer::Style { - text_color: styling.text_color, - }, - content_layout, - cursor_position, - &bounds, - ); - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content - .overlay(layout.children().next().unwrap(), renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - button: Button<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(button) - } -} diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index a49d2fa2b0..dc3c0bd061 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,7 +6,7 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, @@ -168,6 +168,7 @@ where fn on_event( &mut self, + _tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -194,6 +195,7 @@ where fn mouse_interaction( &self, + _tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -208,6 +210,7 @@ where fn draw( &self, + _tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 4eee7d3c43..a8b0f18307 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -4,6 +4,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -19,7 +20,6 @@ pub struct Column<'a, Message, Renderer> { width: Length, height: Length, max_width: u32, - max_height: u32, align_items: Alignment, children: Vec>, } @@ -40,7 +40,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -80,12 +79,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the maximum height of the [`Column`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the horizontal alignment of the contents of the [`Column`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,10 +86,10 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Adds an element to the [`Column`]. - pub fn push(mut self, child: E) -> Self - where - E: Into>, - { + pub fn push( + mut self, + child: impl Into>, + ) -> Self { self.children.push(child.into()); self } @@ -113,6 +106,14 @@ impl<'a, Message, Renderer> Widget where Renderer: crate::Renderer, { + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + fn width(&self) -> Length { self.width } @@ -128,7 +129,6 @@ where ) -> layout::Node { let limits = limits .max_width(self.max_width) - .max_height(self.max_height) .width(self.width) .height(self.height); @@ -143,8 +143,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -154,9 +172,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -170,6 +190,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -177,9 +198,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -192,6 +215,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -199,8 +223,14 @@ where cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw( + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, @@ -211,30 +241,23 @@ where } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from( - column: Column<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) + fn from(column: Column<'a, Message, Renderer>) -> Self { + Self::new(column) } } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 3d68a5959c..2afad3f2d3 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,6 +5,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -121,46 +122,20 @@ where } } -/// Computes the layout of a [`Container`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - max_width: u32, - max_height: u32, - 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() - .max_width(max_width) - .max_height(max_height) - .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 Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { + 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 width(&self) -> Length { self.width } @@ -184,12 +159,30 @@ where self.padding, self.horizontal_alignment, self.vertical_alignment, - |renderer, limits| self.content.layout(renderer, limits), + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -197,7 +190,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.content.widget.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, layout.children().next().unwrap(), cursor_position, @@ -209,12 +203,14 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content.widget.mouse_interaction( + self.content.as_widget().mouse_interaction( + &tree.children[0], layout.children().next().unwrap(), cursor_position, viewport, @@ -224,6 +220,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, renderer_style: &renderer::Style, @@ -235,7 +232,8 @@ where draw_background(renderer, &style, layout.bounds()); - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, theme, &renderer::Style { @@ -249,16 +247,68 @@ where ); } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { - self.content - .overlay(layout.children().next().unwrap(), renderer) + ) -> Option> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + column: Container<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) } } +/// Computes the layout of a [`Container`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_width: u32, + max_height: u32, + 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() + .max_width(max_width) + .max_height(max_height) + .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]) +} + /// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. pub fn draw_background( renderer: &mut Renderer, @@ -281,17 +331,3 @@ pub fn draw_background( ); } } - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} diff --git a/pure/src/helpers.rs b/native/src/widget/helpers.rs similarity index 76% rename from pure/src/helpers.rs rename to native/src/widget/helpers.rs index 88598f9bac..a62448e94c 100644 --- a/pure/src/helpers.rs +++ b/native/src/widget/helpers.rs @@ -1,11 +1,36 @@ //! Helper functions to create pure widgets. use crate::widget; -use crate::Element; +use crate::{Element, Length}; -use iced_native::Length; use std::borrow::Cow; use std::ops::RangeInclusive; +/// Creates a [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +#[macro_export] +macro_rules! column { + () => ( + $crate::widget::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + +/// Creates a [Row`] with the given children. +/// +/// [`Row`]: widget::Row +#[macro_export] +macro_rules! row { + () => ( + $crate::widget::Row::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: widget::Container @@ -13,25 +38,28 @@ pub fn container<'a, Message, Renderer>( content: impl Into>, ) -> widget::Container<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::container::StyleSheet, { widget::Container::new(content) } -/// Creates a new [`Column`]. +/// Creates a new [`Column`] with the given children. /// /// [`Column`]: widget::Column -pub fn column<'a, Message, Renderer>() -> widget::Column<'a, Message, Renderer> -{ - widget::Column::new() +pub fn column( + children: Vec>, +) -> widget::Column<'_, Message, Renderer> { + widget::Column::with_children(children) } -/// Creates a new [`Row`]. +/// Creates a new [`Row`] with the given children. /// /// [`Row`]: widget::Row -pub fn row<'a, Message, Renderer>() -> widget::Row<'a, Message, Renderer> { - widget::Row::new() +pub fn row( + children: Vec>, +) -> widget::Row<'_, Message, Renderer> { + widget::Row::with_children(children) } /// Creates a new [`Scrollable`] with the provided content. @@ -41,7 +69,7 @@ pub fn scrollable<'a, Message, Renderer>( content: impl Into>, ) -> widget::Scrollable<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::scrollable::StyleSheet, { widget::Scrollable::new(content) @@ -54,7 +82,7 @@ pub fn button<'a, Message, Renderer>( content: impl Into>, ) -> widget::Button<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::button::StyleSheet, { widget::Button::new(content) @@ -70,7 +98,7 @@ pub fn tooltip<'a, Message, Renderer>( position: widget::tooltip::Position, ) -> widget::Tooltip<'a, Message, Renderer> where - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet, { widget::Tooltip::new(content, tooltip, position) @@ -79,9 +107,9 @@ where /// Creates a new [`Text`] widget with the provided content. /// /// [`Text`]: widget::Text -pub fn text(text: impl Into) -> widget::Text +pub fn text(text: impl ToString) -> widget::Text where - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::text::StyleSheet, { widget::Text::new(text) @@ -96,7 +124,7 @@ pub fn checkbox<'a, Message, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> widget::Checkbox<'a, Message, Renderer> where - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, { widget::Checkbox::new(is_checked, label, f) @@ -113,7 +141,7 @@ pub fn radio( ) -> widget::Radio where Message: Clone, - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::radio::StyleSheet, V: Copy + Eq, { @@ -129,7 +157,7 @@ pub fn toggler<'a, Message, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> widget::Toggler<'a, Message, Renderer> where - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::toggler::StyleSheet, { widget::Toggler::new(is_checked, label, f) @@ -145,7 +173,7 @@ pub fn text_input<'a, Message, Renderer>( ) -> widget::TextInput<'a, Message, Renderer> where Message: Clone, - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::text_input::StyleSheet, { widget::TextInput::new(placeholder, value, on_change) @@ -162,7 +190,7 @@ pub fn slider<'a, T, Message, Renderer>( where T: Copy + From + std::cmp::PartialOrd, Message: Clone, - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::slider::StyleSheet, { widget::Slider::new(range, value, on_change) @@ -179,7 +207,7 @@ pub fn pick_list<'a, Message, Renderer, T>( where T: ToString + Eq + 'static, [T]: ToOwned>, - Renderer: iced_native::text::Renderer, + Renderer: crate::text::Renderer, Renderer::Theme: widget::pick_list::StyleSheet, { widget::PickList::new(options, selected, on_selected) @@ -211,7 +239,7 @@ pub fn vertical_space(height: Length) -> widget::Space { /// [`Rule`]: widget::Rule pub fn horizontal_rule(height: u16) -> widget::Rule where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::rule::StyleSheet, { widget::Rule::horizontal(height) @@ -222,7 +250,7 @@ where /// [`Rule`]: widget::Rule pub fn vertical_rule(width: u16) -> widget::Rule where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::rule::StyleSheet, { widget::Rule::vertical(width) @@ -240,8 +268,16 @@ pub fn progress_bar( value: f32, ) -> widget::ProgressBar where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, Renderer::Theme: widget::progress_bar::StyleSheet, { widget::ProgressBar::new(range, value) } + +/// Creates a new [`Svg`] widget from the given [`Handle`]. +/// +/// [`Svg`]: widget::Svg +/// [`Handle`]: widget::svg::Handle +pub fn svg(handle: impl Into) -> widget::Svg { + widget::Svg::new(handle) +} diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs new file mode 100644 index 0000000000..4b8fedf1e6 --- /dev/null +++ b/native/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 72075bd18d..91d68e3417 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -5,12 +5,18 @@ pub use viewer::Viewer; use crate::image; use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::hash::Hash; +/// Creates a new [`Viewer`] with the given image `Handle`. +pub fn viewer(handle: Handle) -> Viewer { + Viewer::new(handle) +} + /// A frame that displays an image while keeping aspect ratio. /// /// # Example @@ -135,6 +141,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 1aa75aa064..b1fe596c17 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -4,6 +4,7 @@ use crate::image; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, @@ -13,8 +14,7 @@ use std::hash::Hash; /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] -pub struct Viewer<'a, Handle> { - state: &'a mut State, +pub struct Viewer { padding: u16, width: Length, height: Length, @@ -24,11 +24,10 @@ pub struct Viewer<'a, Handle> { handle: Handle, } -impl<'a, Handle> Viewer<'a, Handle> { +impl Viewer { /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(state: &'a mut State, handle: Handle) -> Self { + pub fn new(handle: Handle) -> Self { Viewer { - state, padding: 0, width: Length::Shrink, height: Length::Shrink, @@ -81,43 +80,21 @@ impl<'a, Handle> Viewer<'a, Handle> { self.scale_step = scale_step; self } - - /// Returns the bounds of the underlying image, given the bounds of - /// the [`Viewer`]. Scaling will be applied and original aspect ratio - /// will be respected. - fn image_size(&self, renderer: &Renderer, bounds: Size) -> Size - where - Renderer: image::Renderer, - { - let (width, height) = renderer.dimensions(&self.handle); - - let (width, height) = { - let dimensions = (width as f32, height as f32); - - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - - let scale = self.state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) - } } -impl<'a, Message, Renderer, Handle> Widget - for Viewer<'a, Handle> +impl Widget for Viewer where Renderer: image::Renderer, Handle: Clone + Hash, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + fn width(&self) -> Length { self.width } @@ -164,6 +141,7 @@ where fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -181,39 +159,43 @@ where match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - let previous_scale = self.state.scale; + let state = tree.state.downcast_mut::(); + let previous_scale = state.scale; if y < 0.0 && previous_scale > self.min_scale || y > 0.0 && previous_scale < self.max_scale { - self.state.scale = (if y > 0.0 { - self.state.scale * (1.0 + self.scale_step) + state.scale = (if y > 0.0 { + state.scale * (1.0 + self.scale_step) } else { - self.state.scale / (1.0 + self.scale_step) + state.scale / (1.0 + self.scale_step) }) .max(self.min_scale) .min(self.max_scale); - let image_size = - self.image_size(renderer, bounds.size()); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); - let factor = - self.state.scale / previous_scale - 1.0; + let factor = state.scale / previous_scale - 1.0; let cursor_to_center = cursor_position - bounds.center(); let adjustment = cursor_to_center * factor - + self.state.current_offset * factor; + + state.current_offset * factor; - self.state.current_offset = Vector::new( + state.current_offset = Vector::new( if image_size.width > bounds.width { - self.state.current_offset.x + adjustment.x + state.current_offset.x + adjustment.x } else { 0.0 }, if image_size.height > bounds.height { - self.state.current_offset.y + adjustment.y + state.current_offset.y + adjustment.y } else { 0.0 }, @@ -227,21 +209,34 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if is_mouse_over => { - self.state.cursor_grabbed_at = Some(cursor_position); - self.state.starting_offset = self.state.current_offset; + let state = tree.state.downcast_mut::(); + + state.cursor_grabbed_at = Some(cursor_position); + state.starting_offset = state.current_offset; event::Status::Captured } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - if self.state.cursor_grabbed_at.is_some() => - { - self.state.cursor_grabbed_at = None; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree.state.downcast_mut::(); - event::Status::Captured + if state.cursor_grabbed_at.is_some() { + state.cursor_grabbed_at = None; + + event::Status::Captured + } else { + event::Status::Ignored + } } Event::Mouse(mouse::Event::CursorMoved { position }) => { - if let Some(origin) = self.state.cursor_grabbed_at { - let image_size = self.image_size(renderer, bounds.size()); + let state = tree.state.downcast_mut::(); + + if let Some(origin) = state.cursor_grabbed_at { + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); let hidden_width = (image_size.width - bounds.width / 2.0) .max(0.0) @@ -255,7 +250,7 @@ where let delta = position - origin; let x = if bounds.width < image_size.width { - (self.state.starting_offset.x - delta.x) + (state.starting_offset.x - delta.x) .min(hidden_width) .max(-hidden_width) } else { @@ -263,14 +258,14 @@ where }; let y = if bounds.height < image_size.height { - (self.state.starting_offset.y - delta.y) + (state.starting_offset.y - delta.y) .min(hidden_height) .max(-hidden_height) } else { 0.0 }; - self.state.current_offset = Vector::new(x, y); + state.current_offset = Vector::new(x, y); event::Status::Captured } else { @@ -283,15 +278,17 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { + let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); - if self.state.is_cursor_grabbed() { + if state.is_cursor_grabbed() { mouse::Interaction::Grabbing } else if is_mouse_over { mouse::Interaction::Grab @@ -302,6 +299,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, @@ -309,9 +307,11 @@ where _cursor_position: Point, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let image_size = self.image_size(renderer, bounds.size()); + let image_size = + image_size(renderer, &self.handle, state, bounds.size()); let translation = { let image_top_left = Vector::new( @@ -319,7 +319,7 @@ where bounds.height / 2.0 - image_size.height / 2.0, ); - image_top_left - self.state.offset(bounds, image_size) + image_top_left - state.offset(bounds, image_size) }; renderer.with_layer(bounds, |renderer| { @@ -385,14 +385,47 @@ impl State { } } -impl<'a, Message, Renderer, Handle> From> +impl<'a, Message, Renderer, Handle> From> for Element<'a, Message, Renderer> where Renderer: 'a + image::Renderer, Message: 'a, Handle: Clone + Hash + 'a, { - fn from(viewer: Viewer<'a, Handle>) -> Element<'a, Message, Renderer> { + fn from(viewer: Viewer) -> Element<'a, Message, Renderer> { Element::new(viewer) } } + +/// Returns the bounds of the underlying image, given the bounds of +/// the [`Viewer`]. Scaling will be applied and original aspect ratio +/// will be respected. +pub fn image_size( + renderer: &Renderer, + handle: &::Handle, + state: &State, + bounds: Size, +) -> Size +where + Renderer: image::Renderer, +{ + let (width, height) = renderer.dimensions(handle); + + let (width, height) = { + let dimensions = (width as f32, height as f32); + + let width_ratio = bounds.width / dimensions.0; + let height_ratio = bounds.height / dimensions.1; + + let ratio = width_ratio.min(height_ratio); + let scale = state.scale; + + if ratio < 1.0 { + (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) + } else { + (dimensions.0 * scale, dimensions.1 * scale) + } + }; + + Size::new(width, height) +} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs new file mode 100644 index 0000000000..ef636aa2ad --- /dev/null +++ b/native/src/widget/operation.rs @@ -0,0 +1,60 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; + +use crate::widget::Id; + +use std::fmt; + +/// A piece of logic that can traverse the widget tree of an application in +/// order to query or update some widget state. +pub trait Operation { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box>), +} + +impl fmt::Debug for Outcome +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({:?})", output), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs new file mode 100644 index 0000000000..f17bf17861 --- /dev/null +++ b/native/src/widget/operation/focusable.rs @@ -0,0 +1,169 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + focused: Option, + + /// The total amount of focusable widgets. + total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus(target: Id) -> impl Operation { + struct Focus { + target: Id, + } + + impl Operation for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// Produces an [`Operation`] that generates a [`Count`] and chains it with the +/// provided function to build a new [`Operation`]. +pub fn count(f: fn(Count) -> O) -> impl Operation +where + O: Operation + 'static, +{ + struct CountFocusable { + count: Count, + next: fn(Count) -> O, + } + + impl Operation for CountFocusable + where + O: Operation + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + Outcome::Chain(Box::new((self.next)(self.count))) + } + } + + CountFocusable { + count: Count::default(), + next: f, + } +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the previous focusable widget. +/// - if not found, focuses the last focusable widget. +pub fn focus_previous() -> impl Operation { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl Operation for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusPrevious { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the next focusable widget. +/// - if not found, focuses the first focusable widget. +pub fn focus_next() -> impl Operation { + struct FocusNext { + count: Count, + current: usize, + } + + impl Operation for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs new file mode 100644 index 0000000000..2210137d53 --- /dev/null +++ b/native/src/widget/operation/scrollable.rs @@ -0,0 +1,35 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage`. + fn snap_to(&mut self, percentage: f32); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to(target: Id, percentage: f32) -> impl Operation { + struct SnapTo { + target: Id, + percentage: f32, + } + + impl Operation for SnapTo { + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.percentage); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + SnapTo { target, percentage } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 70ca772c74..d84fb7a0f9 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -30,6 +30,8 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; +pub use iced_style::pane_grid::{Line, StyleSheet}; + use crate::event::{self, Event}; use crate::layout; use crate::mouse; @@ -37,13 +39,12 @@ use crate::overlay; use crate::renderer; use crate::touch; use crate::widget::container; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -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. /// @@ -66,7 +67,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// ## Example /// /// ``` -/// # use iced_native::widget::{pane_grid, Text}; +/// # use iced_native::widget::{pane_grid, text}; /// # /// # type PaneGrid<'a, Message> = /// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; @@ -84,10 +85,10 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state| { +/// PaneGrid::new(&state, |pane, state| { /// pane_grid::Content::new(match state { -/// PaneState::SomePane => Text::new("This is some pane"), -/// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), /// }) /// }) /// .on_drag(Message::PaneDragged) @@ -99,8 +100,7 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet, { - state: &'a mut state::Internal, - action: &'a mut state::Action, + state: &'a state::Internal, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, @@ -121,21 +121,20 @@ where /// The view function will be called to display each [`Pane`] present in the /// [`State`]. pub fn new( - state: &'a mut State, - view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, + state: &'a State, + view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { state .panes - .iter_mut() + .iter() .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; Self { - state: &mut state.internal, - action: &mut state.action, elements, + state: &state.internal, width: Length::Fill, height: Length::Fill, spacing: 0, @@ -211,6 +210,220 @@ where } } +impl<'a, Message, Renderer> Widget + for PaneGrid<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + 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, + |state, (_, content)| 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 { + 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 = 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 { + 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, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + renderer, + theme, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style, + 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, + theme, + 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 + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} + /// Calculates the [`Layout`] of a [`PaneGrid`]. pub fn layout( renderer: &Renderer, @@ -656,175 +869,6 @@ pub struct ResizeEvent { pub ratio: f32, } -impl<'a, Message, Renderer> Widget - for PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - 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, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - 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() - .zip(layout.children()) - .map(|((pane, content), layout)| { - let is_picked = picked_pane == Some(*pane); - - content.on_event( - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - 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, - ) - }) - .max() - .unwrap_or_default() - }) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - draw( - self.action, - self.state, - layout, - cursor_position, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - self.style, - self.elements.iter().map(|(pane, content)| (*pane, content)), - |pane, renderer, style, layout, cursor_position, rectangle| { - pane.draw( - renderer, - theme, - style, - layout, - cursor_position, - rectangle, - ); - }, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.elements - .iter_mut() - .zip(layout.children()) - .filter_map(|((_, pane), layout)| pane.overlay(layout, renderer)) - .next() - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn from( - pane_grid: PaneGrid<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(pane_grid) - } -} - /* * Helpers */ diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 4c9e65c94f..98ce2c4b96 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -5,6 +5,7 @@ use crate::overlay; use crate::renderer; use crate::widget::container; use crate::widget::pane_grid::{Draggable, TitleBar}; +use crate::widget::Tree; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. @@ -59,11 +60,37 @@ where Renderer: crate::Renderer, Renderer::Theme: container::StyleSheet, { + pub(super) 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(super) 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::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -89,6 +116,7 @@ where let show_controls = bounds.contains(cursor_position); title_bar.draw( + &tree.children[1], renderer, theme, style, @@ -98,7 +126,8 @@ where show_controls, ); - self.body.draw( + self.body.as_widget().draw( + &tree.children[0], renderer, theme, style, @@ -107,7 +136,8 @@ where viewport, ); } else { - self.body.draw( + self.body.as_widget().draw( + &tree.children[0], renderer, theme, style, @@ -131,7 +161,7 @@ where let title_bar_size = title_bar_layout.size(); - let mut body_layout = self.body.layout( + let mut body_layout = self.body.as_widget().layout( renderer, &layout::Limits::new( Size::ZERO, @@ -149,12 +179,13 @@ where vec![title_bar_layout, body_layout], ) } else { - self.body.layout(renderer, limits) + self.body.as_widget().layout(renderer, limits) } } pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -169,6 +200,7 @@ where let mut children = layout.children(); event_status = title_bar.on_event( + &mut tree.children[1], event.clone(), children.next().unwrap(), cursor_position, @@ -185,7 +217,8 @@ where let body_status = if is_picked { event::Status::Ignored } else { - self.body.on_event( + self.body.as_widget_mut().on_event( + &mut tree.children[0], event, body_layout, cursor_position, @@ -200,6 +233,7 @@ where pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -218,6 +252,7 @@ where } let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], title_bar_layout, cursor_position, viewport, @@ -230,25 +265,46 @@ where }; self.body - .mouse_interaction(body_layout, cursor_position, viewport, renderer) + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + viewport, + renderer, + ) .max(title_bar_interaction) } - pub(crate) fn overlay( - &mut self, + 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_mut() { + ) -> Option> { + if let Some(title_bar) = self.title_bar.as_ref() { let mut children = layout.children(); let title_bar_layout = children.next()?; - match title_bar.overlay(title_bar_layout, renderer) { + 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.overlay(children.next()?, renderer), + None => self.body.as_widget().overlay( + body_state, + children.next()?, + renderer, + ), } } else { - self.body.overlay(layout, renderer) + self.body.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) } } } diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 4e90f64543..cdca6267d9 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -31,8 +31,6 @@ pub struct State { /// /// [`PaneGrid`]: crate::widget::PaneGrid pub internal: Internal, - - pub(super) action: Action, } impl State { @@ -54,11 +52,7 @@ impl State { let internal = Internal::from_configuration(&mut panes, config.into(), 0); - State { - panes, - internal, - action: Action::Idle, - } + State { panes, internal } } /// Returns the total amount of panes in the [`State`]. diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 14c3ab4e24..f9050e3e1b 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -4,6 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; +use crate::widget::Tree; use crate::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -86,11 +87,37 @@ where Renderer: crate::Renderer, Renderer::Theme: container::StyleSheet, { + pub(super) 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(super) 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::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, inherited_style: &renderer::Style, @@ -118,14 +145,15 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } if show_controls || self.always_show_controls { - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - controls.draw( + controls.as_widget().draw( + &tree.children[1], renderer, theme, &inherited_style, @@ -137,7 +165,8 @@ where } if show_title { - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, theme, &inherited_style, @@ -186,11 +215,14 @@ where 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(); @@ -221,6 +253,7 @@ where pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -243,7 +276,8 @@ where show_title = false; } - controls.on_event( + controls.as_widget_mut().on_event( + &mut tree.children[1], event.clone(), controls_layout, cursor_position, @@ -256,7 +290,8 @@ where }; let title_status = if show_title { - self.content.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, title_layout, cursor_position, @@ -273,6 +308,7 @@ where pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -284,7 +320,8 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - let title_interaction = self.content.mouse_interaction( + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], title_layout, cursor_position, viewport, @@ -293,7 +330,8 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let controls_interaction = controls.mouse_interaction( + let controls_interaction = controls.as_widget().mouse_interaction( + &tree.children[1], controls_layout, cursor_position, viewport, @@ -312,11 +350,12 @@ where } } - pub(crate) fn overlay( - &mut self, + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { let mut children = layout.children(); let padded = children.next()?; @@ -327,12 +366,23 @@ where content, controls, .. } = self; - content.overlay(title_layout, renderer).or_else(move || { - controls.as_mut().and_then(|controls| { - let controls_layout = children.next()?; - - controls.overlay(controls_layout, renderer) + 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/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 61b0eb9639..1232878bd4 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -9,8 +9,7 @@ use crate::overlay::menu::{self, Menu}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; -use crate::widget::container; -use crate::widget::scrollable; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Widget, @@ -27,8 +26,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, - on_selected: Box Message>, + on_selected: Box Message + 'a>, options: Cow<'a, [T]>, placeholder: Option, selected: Option, @@ -39,35 +37,6 @@ where style: ::Style, } -/// The local state of a [`PickList`]. -#[derive(Debug, Clone)] -pub struct State { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, - last_selection: Option, -} - -impl State { - /// Creates a new [`State`] for a [`PickList`]. - pub fn new() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> where T: ToString + Eq, @@ -78,17 +47,14 @@ where /// 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. + /// Creates a new [`PickList`] with the given list of options, the current + /// selected value, and the message to produce when an option is selected. pub fn new( - state: &'a mut State, options: impl Into>, selected: Option, - on_selected: impl Fn(T) -> Message + 'static, + on_selected: impl Fn(T) -> Message + 'a, ) -> Self { Self { - state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -141,6 +107,168 @@ where } } +impl<'a, T: 'a, Message, Renderer> Widget + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::::new()) + } + + 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_deref(), + &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 { + 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 { + mouse_interaction(layout, cursor_position) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_deref(), + self.selected.as_ref(), + self.style, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + let state = tree.state.downcast_mut::>(); + + overlay( + layout, + state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style, + ) + } +} + +impl<'a, T: 'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { + Self::new(pick_list) + } +} + +/// The local state of a [`PickList`]. +#[derive(Debug)] +pub struct State { + menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, + is_open: bool, + hovered_option: Option, + last_selection: Option, +} + +impl State { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { + Self { + menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + /// Computes the layout of a [`PickList`]. pub fn layout( renderer: &Renderer, @@ -429,127 +557,3 @@ pub fn draw( }); } } - -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, - Renderer::Theme: StyleSheet, -{ - 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_deref(), - &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( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - self.selected.as_ref(), - self.style, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - overlay( - layout, - self.state, - self.padding, - self.text_size, - self.font.clone(), - &self.options, - self.style, - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq, - [T]: ToOwned>, - Message: 'static, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + container::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet, - ::Style: - Into<::Style>, -{ - fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { - Element::new(pick_list) - } -} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 50bdcda60f..8a9454332b 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -1,6 +1,7 @@ //! Provide progress feedback to your users. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; use std::ops::RangeInclusive; @@ -105,6 +106,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 647e3c5ae2..c9152d05cd 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -6,7 +6,7 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Widget, @@ -176,6 +176,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -200,6 +201,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -214,6 +216,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 9d8cc71512..eda7c2d355 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,16 +1,15 @@ //! Distribute content horizontally. use crate::event::{self, Event}; -use crate::layout; +use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Length, Padding, Point, Rectangle, Shell, + Widget, }; -use std::u32; - /// A container that distributes its contents horizontally. #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { @@ -18,8 +17,6 @@ pub struct Row<'a, Message, Renderer> { padding: Padding, width: Length, height: Length, - max_width: u32, - max_height: u32, align_items: Alignment, children: Vec>, } @@ -39,8 +36,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -74,18 +69,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the maximum width of the [`Row`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - - /// Sets the maximum height of the [`Row`]. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the vertical alignment of the contents of the [`Row`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,10 +76,10 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Adds an [`Element`] to the [`Row`]. - pub fn push(mut self, child: E) -> Self - where - E: Into>, - { + pub fn push( + mut self, + child: impl Into>, + ) -> Self { self.children.push(child.into()); self } @@ -113,6 +96,14 @@ impl<'a, Message, Renderer> Widget where Renderer: crate::Renderer, { + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + fn width(&self) -> Length { self.width } @@ -126,11 +117,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height); + let limits = limits.width(self.width).height(self.height); layout::flex::resolve( layout::flex::Axis::Horizontal, @@ -143,8 +130,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -154,9 +159,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -170,6 +177,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -177,9 +185,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -192,6 +202,7 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -199,8 +210,14 @@ where cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw( + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, @@ -211,28 +228,23 @@ where } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { - Element::new(row) + fn from(row: Row<'a, Message, Renderer>) -> Self { + Self::new(row) } } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index f0fda8a9cc..56f8c80d36 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -1,6 +1,7 @@ //! Display a horizontal or vertical rule for dividing content. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; pub use iced_style::rule::{Appearance, FillMode, StyleSheet}; @@ -78,6 +79,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 77ed206671..4ebb07a065 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -5,10 +5,12 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; -use crate::widget::Column; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Alignment, Background, Clipboard, Color, Element, Layout, Length, Padding, - Point, Rectangle, Shell, Size, Vector, Widget, + Background, Clipboard, Color, Command, Element, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::{f32, u32}; @@ -30,13 +32,12 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, + id: Option, height: Length, - max_height: u32, scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - content: Column<'a, Message, Renderer>, + content: Element<'a, Message, Renderer>, on_scroll: Option Message + 'a>>, style: ::Style, } @@ -46,40 +47,23 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - /// Creates a new [`Scrollable`] with the given [`State`]. - pub fn new(state: &'a mut State) -> Self { + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into>) -> Self { Scrollable { - state, + id: None, height: Length::Shrink, - max_height: u32::MAX, scrollbar_width: 10, scrollbar_margin: 0, scroller_width: 10, - content: Column::new(), + content: content.into(), on_scroll: None, style: 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); + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); self } @@ -89,24 +73,6 @@ where self } - /// Sets the maximum width of the [`Scrollable`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.content = self.content.max_width(max_width); - self - } - - /// Sets the maximum height of the [`Scrollable`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_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 { @@ -132,7 +98,7 @@ where /// /// 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 { + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -145,17 +111,235 @@ where self.style = style.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: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::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 width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::::width(self), + self.height, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::(); + + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); + + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); + } + + 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 { + 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.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::(), + renderer, + theme, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref::(), + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content + .as_widget() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = tree + .state + .downcast_ref::() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) } } +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage`. +pub fn snap_to(id: Id, percentage: f32) -> Command { + Command::widget(operation::scrollable::snap_to(id.0, percentage)) +} + /// Computes the layout of a [`Scrollable`]. pub fn layout( renderer: &Renderer, @@ -625,145 +809,6 @@ fn notify_on_scroll( } } -impl<'a, Message, Renderer> Widget - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - 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, - self.max_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( - 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( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - 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( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - self.state, - renderer, - theme, - layout, - cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - self.style, - |renderer, layout, cursor_position, viewport| { - self.content.draw( - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let Self { content, state, .. } = self; - - content - .overlay(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 = state.offset(bounds, content_bounds); - - overlay.translate(Vector::new(0.0, -(offset as f32))) - }) - } -} - /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] pub struct State { @@ -782,6 +827,12 @@ impl Default for State { } } +impl operation::Scrollable for State { + fn snap_to(&mut self, percentage: f32) { + State::snap_to(self, percentage); + } +} + /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { @@ -926,17 +977,3 @@ struct Scroller { /// The bounds of the [`Scroller`]. bounds: Rectangle, } - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - scrollable: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(scrollable) - } -} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index a5ff611c83..585d9c35c9 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -6,6 +6,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget, @@ -29,15 +30,15 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; /// # use iced_native::renderer::Null; /// # /// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; +/// # /// #[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::new(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) @@ -47,11 +48,10 @@ where Renderer: crate::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, range: RangeInclusive, step: T, value: T, - on_change: Box Message>, + on_change: Box Message + 'a>, on_release: Option, width: Length, height: u16, @@ -71,20 +71,14 @@ where /// Creates a new [`Slider`]. /// /// It expects: - /// * the local [`State`] of the [`Slider`] /// * 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( - state: &'a mut State, - range: RangeInclusive, - value: T, - on_change: F, - ) -> Self + pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self where - F: 'static + Fn(T) -> Message, + F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { value @@ -99,7 +93,6 @@ where }; Slider { - state, value, range, step: T::from(1), @@ -150,6 +143,120 @@ where } } +impl<'a, T, Message, Renderer> Widget + for Slider<'a, T, Message, Renderer> +where + T: Copy + Into + num_traits::FromPrimitive, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(self.width).height(Length::Units(self.height)); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + 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, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::(), + self.value, + &self.range, + theme, + self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::(), + ) + } +} + +impl<'a, T, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: Slider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + /// Processes an [`Event`] and updates the [`State`] of a [`Slider`] /// accordingly. pub fn update( @@ -366,102 +473,3 @@ impl State { State::default() } } - -impl<'a, T, Message, Renderer> Widget - for Slider<'a, T, Message, Renderer> -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - 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, - 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.state, - &mut self.value, - &self.range, - self.step, - self.on_change.as_ref(), - &self.on_release, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor_position, - self.state, - self.value, - &self.range, - theme, - self.style, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.state) - } -} - -impl<'a, T, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: 'a + Copy + Into + num_traits::FromPrimitive, - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(slider) - } -} diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index 81338306b6..9f83589386 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -1,6 +1,7 @@ //! Distribute content vertically. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; /// An amount of empty space. @@ -59,6 +60,7 @@ where fn draw( &self, + _state: &Tree, _renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 76b3eb8bcf..aa68bfb851 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,13 +1,16 @@ //! Display vector graphics in your application. use crate::layout; use crate::renderer; -use crate::svg::{self, Handle}; +use crate::svg; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::path::PathBuf; +pub use svg::Handle; + /// A vector graphics image. /// /// An [`Svg`] image resizes smoothly without losing any quality. @@ -109,6 +112,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 8d173b8a84..b30f4518e7 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -3,6 +3,7 @@ use crate::alignment; use crate::layout; use crate::renderer; use crate::text; +use crate::widget::Tree; use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; pub use iced_style::text::{Appearance, StyleSheet}; @@ -44,9 +45,9 @@ where Renderer::Theme: StyleSheet, { /// 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, font: Default::default(), width: Length::Shrink, @@ -145,6 +146,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -243,3 +245,13 @@ where } } } + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(contents: &'a str) -> Self { + Text::new(contents).into() + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index fd360cd727..8ddbc73471 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -19,9 +19,12 @@ use crate::mouse::{self, click}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Color, Command, Element, Layout, Length, Padding, Point, + Rectangle, Shell, Size, Vector, Widget, }; pub use iced_style::text_input::{Appearance, StyleSheet}; @@ -30,20 +33,15 @@ pub use iced_style::text_input::{Appearance, StyleSheet}; /// /// # Example /// ``` -/// # use iced_native::renderer::Null; -/// # use iced_native::widget::text_input; -/// # -/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, iced_native::renderer::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, @@ -57,7 +55,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - state: &'a mut State, + id: Option, placeholder: String, value: Value, is_secure: bool, @@ -80,21 +78,15 @@ where /// 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( - state: &'a mut State, - placeholder: &str, - value: &str, - on_change: F, - ) -> Self + /// - a placeholder, + /// - the current value, and + /// - 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 { - state, + id: None, placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, @@ -109,6 +101,12 @@ where } } + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Converts the [`TextInput`] into a secure password input. pub fn password(mut self) -> Self { self.is_secure = true; @@ -127,7 +125,7 @@ where /// Sets the [`Font`] of the [`TextInput`]. /// - /// [`Font`]: crate::text::Renderer::Font + /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self @@ -166,17 +164,13 @@ where self } - /// Returns the current [`State`] of the [`TextInput`]. - pub fn state(&self) -> &State { - self.state - } - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. + /// [`text_input::Value`] if provided. /// /// [`Renderer`]: text::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, layout: Layout<'_>, @@ -188,7 +182,7 @@ where theme, layout, cursor_position, - self.state, + tree.state.downcast_ref::(), value.unwrap_or(&self.value), &self.placeholder, self.size, @@ -199,6 +193,150 @@ where } } +impl<'a, Message, Renderer> Widget + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + 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 operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::(); + + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + } + + 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 { + 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_paste.as_deref(), + &self.on_submit, + || tree.state.downcast_mut::(), + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::(), + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style, + ) + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: TextInput<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`TextInput`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id.0)) +} + /// Computes the layout of a [`TextInput`]. pub fn layout( renderer: &Renderer, @@ -777,93 +915,6 @@ pub fn mouse_interaction( } } -impl<'a, Message, Renderer> Widget - for TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - 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_paste.as_deref(), - &self.on_submit, - || &mut self.state, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - self.draw(renderer, theme, layout, cursor_position, None) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: TextInput<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} - /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State { @@ -907,6 +958,7 @@ impl State { /// Focuses the [`TextInput`]. pub fn focus(&mut self) { self.is_focused = true; + self.move_cursor_to_end(); } /// Unfocuses the [`TextInput`]. @@ -935,6 +987,20 @@ impl State { } } +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} + mod platform { use crate::keyboard; diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 3deaf28756..7893f78c79 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -5,7 +5,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::text; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Widget, @@ -180,6 +180,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -205,6 +206,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -219,6 +221,7 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs index 0548e9da45..d8198004db 100644 --- a/native/src/widget/tooltip.rs +++ b/native/src/widget/tooltip.rs @@ -6,7 +6,8 @@ use crate::renderer; use crate::text; use crate::widget; use crate::widget::container; -use crate::widget::text::Text; +use crate::widget::overlay; +use crate::widget::{Text, Tree}; use crate::{ Clipboard, Element, Event, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -14,7 +15,7 @@ use crate::{ /// An element to display a widget over another. #[allow(missing_debug_implementations)] -pub struct Tooltip<'a, Message, Renderer> +pub struct Tooltip<'a, Message, Renderer: text::Renderer> where Renderer: text::Renderer, Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, @@ -89,6 +90,153 @@ where } } +impl<'a, Message, Renderer> Widget + for Tooltip<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + 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 width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + 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, + 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, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + ); + + let tooltip = &self.tooltip; + + draw( + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + self.position, + self.gap, + self.padding, + self.style, + |renderer, limits| { + Widget::<(), Renderer>::layout(tooltip, renderer, limits) + }, + |renderer, defaults, layout, cursor_position, viewport| { + Widget::<(), Renderer>::draw( + tooltip, + &Tree::empty(), + renderer, + theme, + defaults, + layout, + cursor_position, + viewport, + ); + }, + ); + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + fn from( + tooltip: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(tooltip) + } +} + /// The position of the tooltip. Defaults to following the cursor. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Position { @@ -220,122 +368,3 @@ pub fn draw( }); } } - -impl<'a, Message, Renderer> Widget - for Tooltip<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.content.width() - } - - fn height(&self) -> Length { - self.content.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - 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 { - self.content.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.content.draw( - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - ); - - let tooltip = &self.tooltip; - - draw( - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - self.position, - self.gap, - self.padding, - self.style, - |renderer, limits| { - Widget::<(), Renderer>::layout(tooltip, renderer, limits) - }, - |renderer, defaults, layout, cursor_position, viewport| { - Widget::<(), Renderer>::draw( - tooltip, - renderer, - theme, - defaults, - layout, - cursor_position, - viewport, - ); - }, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - fn from( - tooltip: Tooltip<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(tooltip) - } -} diff --git a/pure/src/widget/tree.rs b/native/src/widget/tree.rs similarity index 92% rename from pure/src/widget/tree.rs rename to native/src/widget/tree.rs index 2f876523a4..a8b1a18539 100644 --- a/pure/src/widget/tree.rs +++ b/native/src/widget/tree.rs @@ -3,10 +3,12 @@ use crate::Widget; use std::any::{self, Any}; use std::borrow::Borrow; +use std::fmt; /// A persistent state widget tree. /// /// A [`Tree`] is normally associated with a specific widget in the widget tree. +#[derive(Debug)] pub struct Tree { /// The tag of the [`Tree`]. pub tag: Tag, @@ -33,7 +35,7 @@ impl Tree { widget: impl Borrow + 'a>, ) -> Self where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, { let widget = widget.borrow(); @@ -56,7 +58,7 @@ impl Tree { &mut self, new: impl Borrow + 'a>, ) where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, { if self.tag == new.borrow().tag() { new.borrow().diff(self) @@ -70,7 +72,7 @@ impl Tree { &mut self, new_children: &[impl Borrow + 'a>], ) where - Renderer: iced_native::Renderer, + Renderer: crate::Renderer, { self.diff_children_custom( new_children, @@ -174,3 +176,12 @@ impl State { } } } + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::Some(_) => write!(f, "State::Some"), + } + } +} diff --git a/pure/Cargo.toml b/pure/Cargo.toml deleted file mode 100644 index b57e4c5a49..0000000000 --- a/pure/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "iced_pure" -version = "0.2.2" -edition = "2021" -description = "Pure widgets for Iced" -license = "MIT" -repository = "https://github.com/iced-rs/iced" -documentation = "https://docs.rs/iced_pure" -keywords = ["gui", "ui", "graphics", "interface", "widgets"] -categories = ["gui"] - -[dependencies] -iced_native = { version = "0.5", path = "../native" } -iced_style = { version = "0.4", path = "../style" } -num-traits = "0.2" diff --git a/pure/src/element.rs b/pure/src/element.rs deleted file mode 100644 index 35c68716e8..0000000000 --- a/pure/src/element.rs +++ /dev/null @@ -1,346 +0,0 @@ -use crate::overlay; -use crate::widget::tree::{self, Tree}; -use crate::widget::Widget; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -use std::borrow::Borrow; - -/// A generic [`Widget`]. -/// -/// It is useful to build composable user interfaces that do not leak -/// implementation details in their __view logic__. -/// -/// If you have a [built-in widget], you should be able to use `Into` -/// to turn it into an [`Element`]. -/// -/// [built-in widget]: crate::widget -pub struct Element<'a, Message, Renderer> { - widget: Box + 'a>, -} - -impl<'a, Message, Renderer> Element<'a, Message, Renderer> { - /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new(widget: impl Widget + 'a) -> Self - where - Renderer: iced_native::Renderer, - { - Self { - widget: Box::new(widget), - } - } - - /// Returns a reference to the [`Widget`] of the [`Element`], - pub fn as_widget(&self) -> &dyn Widget { - self.widget.as_ref() - } - - /// Returns a mutable reference to the [`Widget`] of the [`Element`], - pub fn as_widget_mut(&mut self) -> &mut dyn Widget { - self.widget.as_mut() - } - - /// Applies a transformation to the produced message of the [`Element`]. - /// - /// This method is useful when you want to decouple different parts of your - /// UI and make them __composable__. - /// - /// # Example - /// Imagine we want to use [our counter](index.html#usage). But instead of - /// showing a single counter, we want to display many of them. We can reuse - /// the `Counter` type as it is! - /// - /// We use composition to model the __state__ of our new application: - /// - /// ``` - /// # mod counter { - /// # pub struct Counter; - /// # } - /// use counter::Counter; - /// - /// struct ManyCounters { - /// counters: Vec, - /// } - /// ``` - /// - /// We can store the state of multiple counters now. However, the - /// __messages__ we implemented before describe the user interactions - /// of a __single__ counter. Right now, we need to also identify which - /// counter is receiving user interactions. Can we use composition again? - /// Yes. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # } - /// #[derive(Debug, Clone, Copy)] - /// pub enum Message { - /// Counter(usize, counter::Message) - /// } - /// ``` - /// - /// We compose the previous __messages__ with the index of the counter - /// producing them. Let's implement our __view logic__ now: - /// - /// ``` - /// # mod counter { - /// # type Text = iced_pure::widget::Text; - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn view(&mut self) -> Text { - /// # Text::new("") - /// # } - /// # } - /// # } - /// # - /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// use iced_pure::Element; - /// use iced_pure::widget::Row; - /// use iced_wgpu::Renderer; - /// - /// impl ManyCounters { - /// pub fn view(&mut self) -> Row { - /// // We can quickly populate a `Row` by folding over our counters - /// self.counters.iter_mut().enumerate().fold( - /// Row::new().spacing(20), - /// |row, (index, counter)| { - /// // We display the counter - /// let element: Element = - /// counter.view().into(); - /// - /// row.push( - /// // Here we turn our `Element` into - /// // an `Element` by combining the `index` and the - /// // message of the `element`. - /// element.map(move |message| Message::Counter(index, message)) - /// ) - /// } - /// ) - /// } - /// } - /// ``` - /// - /// Finally, our __update logic__ is pretty straightforward: simple - /// delegation. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn update(&mut self, _message: Message) {} - /// # } - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// impl ManyCounters { - /// pub fn update(&mut self, message: Message) { - /// match message { - /// Message::Counter(index, counter_msg) => { - /// if let Some(counter) = self.counters.get_mut(index) { - /// counter.update(counter_msg); - /// } - /// } - /// } - /// } - /// } - /// ``` - 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) -> tree::Tag { - self.widget.tag() - } - - fn state(&self) -> tree::State { - self.widget.state() - } - - fn children(&self) -> Vec { - self.widget.children() - } - - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree) - } - - fn width(&self) -> Length { - self.widget.width() - } - - fn height(&self) -> Length { - self.widget.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, B>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let status = self.widget.on_event( - tree, - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ); - - shell.merge(local_shell, &self.mapper); - - status - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget.draw( - tree, - renderer, - theme, - 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 overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let mapper = &self.mapper; - - self.widget - .overlay(tree, layout, renderer) - .map(move |overlay| overlay.map(mapper)) - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for &Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} diff --git a/pure/src/flex.rs b/pure/src/flex.rs deleted file mode 100644 index 3f65a28b62..0000000000 --- a/pure/src/flex.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! 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<'_, Message, Renderer>], -) -> Node -where - Renderer: iced_native::Renderer, -{ - let limits = limits.pad(padding); - let total_spacing = spacing * items.len().saturating_sub(1) as f32; - let max_cross = axis.cross(limits.max()); - - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); - let mut available = axis.main(limits.max()) - total_spacing; - - let mut nodes: Vec = Vec::with_capacity(items.len()); - nodes.resize(items.len(), Node::default()); - - if align_items == Alignment::Fill { - let mut fill_cross = axis.cross(limits.min()); - - items.iter().for_each(|child| { - let cross_fill_factor = match axis { - Axis::Horizontal => child.as_widget().height(), - Axis::Vertical => child.as_widget().width(), - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - }); - - cross = fill_cross; - } - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(size)); - } - - nodes[i] = layout; - } else { - fill_sum += fill_factor; - } - } - - let remaining = available.max(0.0); - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor != 0 { - let max_main = remaining * fill_factor as f32 / fill_sum as f32; - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; - - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(layout.size())); - } - - nodes[i] = layout; - } - } - - let pad = axis.pack(padding.left as f32, padding.top as f32); - let mut main = pad.0; - - for (i, node) in nodes.iter_mut().enumerate() { - if i > 0 { - main += spacing; - } - - let (x, y) = axis.pack(main, pad.1); - - node.move_to(Point::new(x, y)); - - match axis { - Axis::Horizontal => { - node.align( - Alignment::Start, - align_items, - Size::new(0.0, cross), - ); - } - Axis::Vertical => { - node.align( - align_items, - Alignment::Start, - Size::new(cross, 0.0), - ); - } - } - - let size = node.size(); - - main += axis.main(size); - } - - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height)); - - Node::with_children(size.pad(padding), nodes) -} diff --git a/pure/src/lib.rs b/pure/src/lib.rs deleted file mode 100644 index 9c5f3bc863..0000000000 --- a/pure/src/lib.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Stateless, pure widgets for iced. -//! -//! # The Elm Architecture, purity, and continuity -//! As you may know, applications made with `iced` use [The Elm Architecture]. -//! -//! In a nutshell, this architecture defines the initial state of the application, a way to `view` it, and a way to `update` it after a user interaction. The `update` logic is called after a meaningful user interaction, which in turn updates the state of the application. Then, the `view` logic is executed to redisplay the application. -//! -//! Since `view` logic is only run after an `update`, all of the mutations to the application state must only happen in the `update` logic. If the application state changes anywhere else, the `view` logic will not be rerun and, therefore, the previously generated `view` may stay outdated. -//! -//! However, the `Application` trait in `iced` defines `view` as: -//! -//! ```ignore -//! pub trait Application { -//! fn view(&mut self) -> Element; -//! } -//! ``` -//! -//! As a consequence, the application state can be mutated in `view` logic. The `view` logic in `iced` is __impure__. -//! -//! This impurity is necessary because `iced` puts the burden of widget __continuity__ on its users. In other words, it's up to you to provide `iced` with the internal state of each widget every time `view` is called. -//! -//! If we take a look at the classic `counter` example: -//! -//! ```ignore -//! struct Counter { -//! value: i32, -//! increment_button: button::State, -//! decrement_button: button::State, -//! } -//! -//! // ... -//! -//! impl Counter { -//! pub fn view(&mut self) -> Column { -//! Column::new() -//! .push( -//! Button::new(&mut self.increment_button, Text::new("+")) -//! .on_press(Message::IncrementPressed), -//! ) -//! .push(Text::new(self.value.to_string()).size(50)) -//! .push( -//! Button::new(&mut self.decrement_button, Text::new("-")) -//! .on_press(Message::DecrementPressed), -//! ) -//! } -//! } -//! ``` -//! -//! We can see how we need to keep track of the `button::State` of each `Button` in our `Counter` state and provide a mutable reference to the widgets in our `view` logic. The widgets produced by `view` are __stateful__. -//! -//! While this approach forces users to keep track of widget state and causes impurity, I originally chose it because it allows `iced` to directly consume the widget tree produced by `view`. Since there is no internal state decoupled from `view` maintained by the runtime, `iced` does not need to compare (e.g. reconciliate) widget trees in order to ensure continuity. -//! -//! # Stateless widgets -//! As the library matures, the need for some kind of persistent widget data (see #553) between `view` calls becomes more apparent (e.g. incremental rendering, animations, accessibility, etc.). -//! -//! If we are going to end up having persistent widget data anyways... There is no reason to have impure, stateful widgets anymore! -//! -//! And so I started exploring and ended up creating a new subcrate called `iced_pure`, which introduces a completely stateless implementation for every widget in `iced`. -//! -//! With the help of this crate, we can now write a pure `counter` example: -//! -//! ```ignore -//! struct Counter { -//! value: i32, -//! } -//! -//! // ... -//! -//! impl Counter { -//! fn view(&self) -> Column { -//! Column::new() -//! .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)) -//! } -//! } -//! ``` -//! -//! Notice how we no longer need to keep track of the `button::State`! The widgets in `iced_pure` do not take any mutable application state in `view`. They are __stateless__ widgets. As a consequence, we do not need mutable access to `self` in `view` anymore. `view` becomes __pure__. -//! -//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![deny( - missing_docs, - unused_results, - clippy::extra_unused_lifetimes, - clippy::from_over_into, - clippy::needless_borrow, - clippy::new_without_default, - clippy::useless_conversion -)] -#![forbid(rust_2018_idioms, unsafe_code)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] - -pub mod flex; -pub mod helpers; -pub mod overlay; -pub mod 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}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -/// A bridge between impure and pure widgets. -/// -/// If you already have an existing `iced` application, you do not need to switch completely to the new traits in order to benefit from the `pure` module. Instead, you can leverage the new `Pure` widget to include `pure` widgets in your impure `Application`. -/// -/// For instance, let's say we want to use our pure `Counter` in an impure application: -/// -/// ```ignore -/// use iced_pure::{self, Pure}; -/// -/// struct Impure { -/// state: pure::State, -/// counter: Counter, -/// } -/// -/// impl Sandbox for Impure { -/// // ... -/// -/// pub fn view(&mut self) -> Element { -/// Pure::new(&mut self.state, self.counter.view()).into() -/// } -/// } -/// ``` -/// -/// [`Pure`] acts as a bridge between pure and impure widgets. It is completely opt-in and can be used to slowly migrate your application to the new architecture. -/// -/// The purification of your application may trigger a bunch of important refactors, since it's far easier to keep your data decoupled from the GUI state with stateless widgets. For this reason, I recommend starting small in the most nested views of your application and slowly expand the purity upwards. -pub struct Pure<'a, Message, Renderer> { - state: &'a mut State, - element: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Pure<'a, Message, Renderer> -where - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - /// Creates a new [`Pure`] widget with the given [`State`] and impure [`Element`]. - pub fn new( - state: &'a mut State, - content: impl Into>, - ) -> Self { - let element = content.into(); - state.diff(&element); - - Self { state, element } - } -} - -/// The internal state of a [`Pure`] widget. -pub struct State { - state_tree: widget::Tree, -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -impl State { - /// Creates a new [`State`] for a [`Pure`] widget. - pub fn new() -> Self { - Self { - state_tree: widget::Tree::empty(), - } - } - - fn diff( - &mut self, - new_element: &Element<'_, Message, Renderer>, - ) where - Renderer: iced_native::Renderer, - { - self.state_tree.diff(new_element); - } -} - -impl<'a, Message, Renderer> iced_native::Widget - for Pure<'a, Message, Renderer> -where - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - fn width(&self) -> Length { - self.element.as_widget().width() - } - - fn height(&self) -> Length { - self.element.as_widget().height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.element.as_widget().layout(renderer, limits) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.element.as_widget_mut().on_event( - &mut self.state.state_tree, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.element.as_widget().draw( - &self.state.state_tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.as_widget().mouse_interaction( - &self.state.state_tree, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.element.as_widget_mut().overlay( - &mut self.state.state_tree, - layout, - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for iced_native::Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - fn from(pure: Pure<'a, Message, Renderer>) -> Self { - Self::new(pure) - } -} diff --git a/pure/src/overlay.rs b/pure/src/overlay.rs deleted file mode 100644 index b82d8a67de..0000000000 --- a/pure/src/overlay.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Display interactive elements on top of other widgets. -use crate::widget::Tree; - -use iced_native::Layout; - -pub use iced_native::overlay::*; - -/// Obtains the first overlay [`Element`] found in the given children. -/// -/// This method will generally only be used by advanced users that are -/// implementing the [`Widget`](crate::Widget) trait. -pub fn from_children<'a, Message, Renderer>( - children: &'a [crate::Element<'_, Message, Renderer>], - tree: &'a mut Tree, - layout: Layout<'_>, - renderer: &Renderer, -) -> Option> -where - Renderer: iced_native::Renderer, -{ - children - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|((child, state), layout)| { - child.as_widget().overlay(state, layout, renderer) - }) - .next() -} diff --git a/pure/src/widget.rs b/pure/src/widget.rs deleted file mode 100644 index cd825ad877..0000000000 --- a/pure/src/widget.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! Use the built-in widgets or create your own. -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; -pub mod text_input; -pub mod toggler; -pub mod tooltip; -pub mod tree; - -mod column; -mod row; -mod space; - -pub use button::Button; -pub use checkbox::Checkbox; -pub use column::Column; -pub use container::Container; -pub use image::Image; -pub use pane_grid::PaneGrid; -pub use pick_list::PickList; -pub use progress_bar::ProgressBar; -pub use radio::Radio; -pub use row::Row; -pub use rule::Rule; -pub use scrollable::Scrollable; -pub use slider::Slider; -pub use space::Space; -pub use svg::Svg; -pub use text::Text; -pub use text_input::TextInput; -pub use toggler::Toggler; -pub use tooltip::{Position, Tooltip}; -pub use tree::Tree; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -/// A component that displays information and allows interaction. -/// -/// If you want to build your own widgets, you will need to implement this -/// trait. -pub trait Widget -where - Renderer: iced_native::Renderer, -{ - /// Returns the width of the [`Widget`]. - fn width(&self) -> Length; - - /// Returns the height of the [`Widget`]. - fn height(&self) -> Length; - - /// Returns the [`layout::Node`] of the [`Widget`]. - /// - /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the - /// user interface. - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node; - - /// Draws the [`Widget`] using the associated `Renderer`. - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ); - - /// Returns the [`Tag`] of the [`Widget`]. - /// - /// [`Tag`]: tree::Tag - fn tag(&self) -> tree::Tag { - tree::Tag::stateless() - } - - /// Returns the [`State`] of the [`Widget`]. - /// - /// [`State`]: tree::State - fn state(&self) -> tree::State { - tree::State::None - } - - /// Returns the state [`Tree`] of the children of the [`Widget`]. - fn children(&self) -> Vec { - Vec::new() - } - - /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} - - /// Processes a runtime [`Event`]. - /// - /// By default, it does nothing. - 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 - } - - /// Returns the current [`mouse::Interaction`] of the [`Widget`]. - /// - /// By default, it returns [`mouse::Interaction::Idle`]. - fn mouse_interaction( - &self, - _state: &Tree, - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse::Interaction::Idle - } - - /// Returns the overlay of the [`Widget`], if there is any. - fn overlay<'a>( - &'a self, - _state: &'a mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} diff --git a/pure/src/widget/button.rs b/pure/src/widget/button.rs deleted file mode 100644 index eb174e5731..0000000000 --- a/pure/src/widget/button.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -use crate::overlay; -use crate::widget::tree::{self, Tree}; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::widget::button; -use iced_native::{ - Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, -}; - -pub use iced_style::button::{Appearance, StyleSheet}; - -use button::State; - -/// A generic widget that produces a message when pressed. -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_pure::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); -/// ``` -/// -/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will -/// be disabled: -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_pure::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) -/// } -/// ``` -pub struct Button<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - content: Element<'a, Message, Renderer>, - on_press: Option, - width: Length, - height: Length, - padding: Padding, - style: ::Style, -} - -impl<'a, Message, Renderer> Button<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Button`] with the given content. - pub fn new(content: impl Into>) -> Self { - Button { - content: content.into(), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(5), - style: ::Style::default(), - } - } - - /// 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 variant of this [`Button`]. - pub fn style( - mut self, - style: ::Style, - ) -> Self { - self.style = style; - self - } -} - -impl<'a, Message, Renderer> Widget - for Button<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::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 width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - button::layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) { - return event::Status::Captured; - } - - button::update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || tree.state.downcast_mut::(), - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - - let styling = button::draw( - renderer, - bounds, - cursor_position, - self.on_press.is_some(), - theme, - self.style, - || tree.state.downcast_ref::(), - ); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: styling.text_color, - }, - content_layout, - cursor_position, - &bounds, - ); - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - button::mouse_interaction( - layout, - cursor_position, - self.on_press.is_some(), - ) - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: iced_native::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(button: Button<'a, Message, Renderer>) -> Self { - Self::new(button) - } -} diff --git a/pure/src/widget/checkbox.rs b/pure/src/widget/checkbox.rs deleted file mode 100644 index e0f9b76432..0000000000 --- a/pure/src/widget/checkbox.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::text; -use iced_native::widget; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::checkbox::{Appearance, Checkbox, StyleSheet}; - -impl<'a, Message, Renderer> Widget - for Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from(checkbox: Checkbox<'a, Message, Renderer>) -> Self { - Self::new(checkbox) - } -} diff --git a/pure/src/widget/column.rs b/pure/src/widget/column.rs deleted file mode 100644 index 027eff0a09..0000000000 --- a/pure/src/widget/column.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::flex; -use crate::overlay; -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{ - Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, -}; - -use std::u32; - -/// A container that distributes its contents vertically. -pub struct Column<'a, Message, Renderer> { - spacing: u16, - padding: Padding, - width: Length, - height: Length, - max_width: u32, - align_items: Alignment, - children: Vec>, -} - -impl<'a, Message, Renderer> Column<'a, Message, Renderer> { - /// Creates an empty [`Column`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Column { - spacing: 0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: u32::MAX, - align_items: Alignment::Start, - children, - } - } - - /// 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.spacing = units; - self - } - - /// Sets the [`Padding`] of the [`Column`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Column`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the height of the [`Column`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// Sets the maximum width of the [`Column`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - - /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an element to the [`Column`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Column<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); - - flex::resolve( - flex::Axis::Vertical, - renderer, - &limits, - self.padding, - self.spacing as f32, - self.align_items, - &self.children, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - 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, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - fn from(column: Column<'a, Message, Renderer>) -> Self { - Self::new(column) - } -} diff --git a/pure/src/widget/container.rs b/pure/src/widget/container.rs deleted file mode 100644 index 44ffff8c2d..0000000000 --- a/pure/src/widget/container.rs +++ /dev/null @@ -1,263 +0,0 @@ -//! Decorate content and apply alignment. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::alignment; -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::container; -use iced_native::{ - Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, -}; - -use std::u32; - -pub use iced_style::container::{Appearance, StyleSheet}; - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - padding: Padding, - width: Length, - height: Length, - max_width: u32, - max_height: u32, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - style: ::Style, - content: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Container<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// 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: 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: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Container<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - 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 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.max_width, - self.max_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, - theme: &Renderer::Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let style = theme.appearance(self.style); - - container::draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} diff --git a/pure/src/widget/image.rs b/pure/src/widget/image.rs deleted file mode 100644 index 58f81a6f9c..0000000000 --- a/pure/src/widget/image.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Display images in your user interface. -use crate::widget::{Tree, Widget}; -use crate::Element; - -use iced_native::layout::{self, Layout}; -use iced_native::renderer; -use iced_native::widget::image; -use iced_native::{Length, Point, Rectangle}; - -use std::hash::Hash; - -pub use image::Image; - -impl Widget for Image -where - Handle: Clone + Hash, - Renderer: iced_native::image::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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } -} - -impl<'a, Message, Renderer, Handle> From> - for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: iced_native::image::Renderer + 'a, - Handle: Clone + Hash + 'a, -{ - fn from(image: Image) -> Self { - Self::new(image) - } -} diff --git a/pure/src/widget/pane_grid.rs b/pure/src/widget/pane_grid.rs deleted file mode 100644 index 69150aa87b..0000000000 --- a/pure/src/widget/pane_grid.rs +++ /dev/null @@ -1,414 +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.4/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::container; -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; -/// # use iced_pure::text; -/// # -/// # type PaneGrid<'a, Message> = -/// # iced_pure::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; -/// # -/// enum PaneState { -/// SomePane, -/// AnotherKindOfPane, -/// } -/// -/// enum Message { -/// PaneDragged(pane_grid::DragEvent), -/// PaneResized(pane_grid::ResizeEvent), -/// } -/// -/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); -/// -/// let pane_grid = -/// PaneGrid::new(&state, |pane, state| { -/// pane_grid::Content::new(match state { -/// PaneState::SomePane => text("This is some pane"), -/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), -/// }) -/// }) -/// .on_drag(Message::PaneDragged) -/// .on_resize(10, Message::PaneResized); -/// ``` -#[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - 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: ::Style, -} - -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - /// 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: 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<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for PaneGrid<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - 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, - |state, (_, content)| 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - pane_grid::draw( - tree.state.downcast_ref(), - self.state, - layout, - cursor_position, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - self.style, - 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, - theme, - 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 - Message: 'a, - Renderer: 'a + iced_native::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - 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 deleted file mode 100644 index 9c2a0f4ac4..0000000000 --- a/pure/src/widget/pane_grid/content.rs +++ /dev/null @@ -1,345 +0,0 @@ -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> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - title_bar: Option>, - body: Element<'a, Message, Renderer>, - style: ::Style, -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// Creates a new [`Content`] with the provided body. - pub fn new(body: impl Into>) -> Self { - Self { - title_bar: None, - body: body.into(), - style: 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: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - pub(super) 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(super) 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`]: iced_native::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - - { - let style = theme.appearance(self.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, - theme, - style, - title_bar_layout, - cursor_position, - viewport, - show_controls, - ); - - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - body_layout, - cursor_position, - viewport, - ); - } else { - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - 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, - Renderer::Theme: container::StyleSheet, -{ - 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, - Renderer::Theme: container::StyleSheet, -{ - 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 deleted file mode 100644 index de9591a202..0000000000 --- a/pure/src/widget/pane_grid/title_bar.rs +++ /dev/null @@ -1,388 +0,0 @@ -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> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - content: Element<'a, Message, Renderer>, - controls: Option>, - padding: Padding, - always_show_controls: bool, - style: ::Style, -} - -impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// 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: 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<::Style>, - ) -> Self { - self.style = 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, - Renderer::Theme: container::StyleSheet, -{ - pub(super) 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(super) 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`]: iced_native::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - show_controls: bool, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - let style = theme.appearance(self.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(); - let mut show_title = true; - - if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - if show_controls || self.always_show_controls { - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor_position, - viewport, - ); - } - } - - if show_title { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &inherited_style, - title_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 mut show_title = true; - - let control_status = if let Some(controls) = &mut self.controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - 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 = if show_title { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - title_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - 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(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor_position, - viewport, - renderer, - ); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - controls_interaction - } else { - controls_interaction.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/pick_list.rs b/pure/src/widget/pick_list.rs deleted file mode 100644 index 9264544ac4..0000000000 --- a/pure/src/widget/pick_list.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Display a dropdown list of selectable values. -use crate::widget::tree::{self, Tree}; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::text; -use iced_native::widget::pick_list; -use iced_native::{ - Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, -}; - -use std::borrow::Cow; - -pub use iced_style::pick_list::{Appearance, StyleSheet}; - -/// A widget for selecting a single value from a list of options. -#[allow(missing_debug_implementations)] -pub struct PickList<'a, T, Message, Renderer> -where - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - on_selected: Box Message + 'a>, - options: Cow<'a, [T]>, - placeholder: Option, - selected: Option, - width: Length, - padding: Padding, - text_size: Option, - font: Renderer::Font, - style: ::Style, -} - -impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> -where - T: ToString + Eq, - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default padding of a [`PickList`]. - pub const DEFAULT_PADDING: Padding = Padding::new(5); - - /// Creates a new [`PickList`] with the given 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: 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: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, T: 'a, Message, Renderer> Widget - for PickList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - 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_deref(), - &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, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - pick_list::draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - &self.font, - self.placeholder.as_deref(), - self.selected.as_ref(), - self.style, - ) - } - - fn overlay<'b>( - &'b 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, - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { - Self::new(pick_list) - } -} diff --git a/pure/src/widget/progress_bar.rs b/pure/src/widget/progress_bar.rs deleted file mode 100644 index c96448531e..0000000000 --- a/pure/src/widget/progress_bar.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Provide progress feedback to your users. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::progress_bar::*; - -impl Widget for ProgressBar -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(progress_bar: ProgressBar) -> Self { - Self::new(progress_bar) - } -} diff --git a/pure/src/widget/radio.rs b/pure/src/widget/radio.rs deleted file mode 100644 index 604c2785f9..0000000000 --- a/pure/src/widget/radio.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Create choices using radio buttons. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::text; -use iced_native::widget; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::radio::{Appearance, Radio, StyleSheet}; - -impl Widget for Radio -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from(radio: Radio) -> Self { - Self::new(radio) - } -} diff --git a/pure/src/widget/row.rs b/pure/src/widget/row.rs deleted file mode 100644 index a288a68df6..0000000000 --- a/pure/src/widget/row.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::flex; -use crate::overlay; -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{ - Alignment, Clipboard, Length, Padding, Point, Rectangle, Shell, -}; - -/// A container that distributes its contents horizontally. -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> { - /// Creates an empty [`Row`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Row`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Row { - spacing: 0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align_items: Alignment::Start, - children, - } - } - - /// Sets the horizontal 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.spacing = units; - self - } - - /// Sets the [`Padding`] of the [`Row`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Row`]. - pub fn width(mut self, width: Length) -> Self { - self.width = width; - self - } - - /// Sets the height of the [`Row`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an [`Element`] to the [`Row`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Row<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - flex::resolve( - flex::Axis::Horizontal, - renderer, - &limits, - self.padding, - self.spacing as f32, - self.align_items, - &self.children, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - 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, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - fn from(row: Row<'a, Message, Renderer>) -> Self { - Self::new(row) - } -} diff --git a/pure/src/widget/rule.rs b/pure/src/widget/rule.rs deleted file mode 100644 index 0fb4ebab7f..0000000000 --- a/pure/src/widget/rule.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::rule::*; - -impl Widget for Rule -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(rule: Rule) -> Self { - Self::new(rule) - } -} diff --git a/pure/src/widget/scrollable.rs b/pure/src/widget/scrollable.rs deleted file mode 100644 index 4118b67eff..0000000000 --- a/pure/src/widget/scrollable.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::overlay; -use crate::widget::tree::{self, Tree}; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::widget::scrollable; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell, Vector}; - -pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; - -/// A widget that can vertically display an infinite amount of content with a -/// scrollbar. -#[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - height: Length, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, - style: ::Style, -} - -impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - /// 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: content.into(), - on_scroll: None, - style: Default::default(), - } - } - - /// Sets the height of the [`Scrollable`]. - pub fn height(mut self, height: Length) -> Self { - self.height = height; - self - } - - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. - pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { - self.scrollbar_width = scrollbar_width.max(1); - self - } - - /// Sets the scrollbar margin of the [`Scrollable`] . - pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { - self.scrollbar_margin = scrollbar_margin; - self - } - - /// Sets the scroller width of the [`Scrollable`] . - /// - /// It silently enforces a minimum value of 1. - pub fn scroller_width(mut self, scroller_width: u16) -> Self { - self.scroller_width = scroller_width.max(1); - self - } - - /// Sets a function to call when the [`Scrollable`] is scrolled. - /// - /// The function takes the new relative offset of the [`Scrollable`] - /// (e.g. `0` means top, while `1` means bottom). - pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { - self.on_scroll = Some(Box::new(f)); - self - } - - /// Sets the style of the [`Scrollable`] . - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Scrollable<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(scrollable::State::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 width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - scrollable::layout( - renderer, - limits, - Widget::::width(self), - self.height, - u32::MAX, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - scrollable::update( - tree.state.downcast_mut::(), - event, - layout, - cursor_position, - clipboard, - shell, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - scrollable::draw( - tree.state.downcast_ref::(), - renderer, - theme, - layout, - cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, - self.style, - |renderer, layout, cursor_position, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - 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.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> { - self.content - .as_widget() - .overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - .map(|overlay| { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::() - .offset(bounds, content_bounds); - - overlay.translate(Vector::new(0.0, -(offset as f32))) - }) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} diff --git a/pure/src/widget/slider.rs b/pure/src/widget/slider.rs deleted file mode 100644 index fed979e5b9..0000000000 --- a/pure/src/widget/slider.rs +++ /dev/null @@ -1,255 +0,0 @@ -//! Display an interactive selector of a single value from a range of 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::renderer; -use iced_native::widget::slider; -use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell, Size}; - -use std::ops::RangeInclusive; - -pub use iced_style::slider::{Appearance, Handle, HandleShape, 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_pure::widget::slider; -/// # use iced_native::renderer::Null; -/// # -/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; -/// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), -/// } -/// -/// let value = 50.0; -/// -/// Slider::new(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, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - range: RangeInclusive, - step: T, - value: T, - on_change: Box Message + 'a>, - on_release: Option, - width: Length, - height: u16, - style: ::Style, -} - -impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - /// 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: 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: impl Into<::Style>, - ) -> Self { - self.style = style.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, Renderer> -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(slider::State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = - limits.width(self.width).height(Length::Units(self.height)); - - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - slider::update( - event, - layout, - cursor_position, - shell, - tree.state.downcast_mut::(), - &mut self.value, - &self.range, - self.step, - self.on_change.as_ref(), - &self.on_release, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - slider::draw( - renderer, - layout, - cursor_position, - tree.state.downcast_ref::(), - self.value, - &self.range, - theme, - self.style, - ) - } - - 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::(), - ) - } -} - -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, - Renderer::Theme: StyleSheet, -{ - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(slider) - } -} diff --git a/pure/src/widget/space.rs b/pure/src/widget/space.rs deleted file mode 100644 index 408cb6470f..0000000000 --- a/pure/src/widget/space.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::Space; - -impl Widget for Space -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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, -{ - fn from(space: Space) -> Self { - Self::new(space) - } -} diff --git a/pure/src/widget/svg.rs b/pure/src/widget/svg.rs deleted file mode 100644 index ae4e8cffce..0000000000 --- a/pure/src/widget/svg.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Display vector graphics in your application. -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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } -} - -impl<'a, Message, Renderer> From for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: iced_native::svg::Renderer + 'a, -{ - fn from(svg: Svg) -> Self { - Self::new(svg) - } -} diff --git a/pure/src/widget/text.rs b/pure/src/widget/text.rs deleted file mode 100644 index 7c6f6ce977..0000000000 --- a/pure/src/widget/text.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Write some text for your users to read. -use crate::widget::Tree; -use crate::{Element, Widget}; - -use iced_native::layout::{self, Layout}; -use iced_native::renderer; -use iced_native::text; -use iced_native::widget; -use iced_native::{Length, Point, Rectangle}; - -pub use iced_native::widget::text::{Appearance, StyleSheet, Text}; - -impl Widget for Text -where - Renderer: text::Renderer, - Renderer::Theme: widget::text::StyleSheet, -{ - 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, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: text::Renderer + 'a, - Renderer::Theme: widget::text::StyleSheet, -{ - fn from(text: Text) -> Self { - Self::new(text) - } -} - -impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> -where - Renderer: text::Renderer + 'a, - Renderer::Theme: widget::text::StyleSheet, -{ - fn from(contents: &'a str) -> Self { - Text::new(contents).into() - } -} diff --git a/pure/src/widget/text_input.rs b/pure/src/widget/text_input.rs deleted file mode 100644 index 514a6795c7..0000000000 --- a/pure/src/widget/text_input.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Display fields that can be filled with text. -use crate::widget::tree::{self, Tree}; -use crate::{Element, Widget}; - -use iced_native::event::{self, Event}; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::text; -use iced_native::widget::text_input; -use iced_native::{Clipboard, Length, Padding, Point, Rectangle, Shell}; - -pub use iced_style::text_input::{Appearance, StyleSheet}; - -/// A field that can be filled with text. -/// -/// # Example -/// ``` -/// # pub type TextInput<'a, Message> = iced_pure::widget::TextInput<'a, Message, iced_native::renderer::Null>; -/// #[derive(Debug, Clone)] -/// enum Message { -/// TextInputChanged(String), -/// } -/// -/// let value = "Some text"; -/// -/// let input = TextInput::new( -/// "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> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - placeholder: String, - value: text_input::Value, - is_secure: bool, - font: Renderer::Font, - width: Length, - padding: Padding, - size: Option, - on_change: Box Message + 'a>, - on_paste: Option Message + 'a>>, - on_submit: Option, - style: ::Style, -} - -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value, and - /// - 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_paste: None, - on_submit: None, - style: Default::default(), - } - } - - /// Converts the [`TextInput`] into a secure password input. - pub fn password(mut self) -> Self { - self.is_secure = true; - self - } - - /// Sets the message that should be produced when some text is pasted into - /// the [`TextInput`]. - pub fn on_paste( - mut self, - on_paste: impl Fn(String) -> Message + 'a, - ) -> Self { - self.on_paste = Some(Box::new(on_paste)); - self - } - - /// Sets the [`Font`] of the [`TextInput`]. - /// - /// [`Font`]: text::Renderer::Font - 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: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`text_input::Value`] if provided. - /// - /// [`Renderer`]: text::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - value: Option<&text_input::Value>, - ) { - text_input::draw( - renderer, - theme, - layout, - cursor_position, - tree.state.downcast_ref::(), - value.unwrap_or(&self.value), - &self.placeholder, - self.size, - &self.font, - self.is_secure, - self.style, - ) - } -} - -impl<'a, Message, Renderer> Widget - for TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(text_input::State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - text_input::layout( - renderer, - limits, - self.width, - self.padding, - self.size, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - text_input::update( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - &self.font, - self.is_secure, - self.on_change.as_ref(), - self.on_paste.as_deref(), - &self.on_submit, - || tree.state.downcast_mut::(), - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - text_input::draw( - renderer, - theme, - layout, - cursor_position, - tree.state.downcast_ref::(), - &self.value, - &self.placeholder, - self.size, - &self.font, - self.is_secure, - self.style, - ) - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - text_input::mouse_interaction(layout, cursor_position) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: TextInput<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} diff --git a/pure/src/widget/toggler.rs b/pure/src/widget/toggler.rs deleted file mode 100644 index 8d0044d296..0000000000 --- a/pure/src/widget/toggler.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Show toggle controls using togglers. -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::widget; -use iced_native::{Clipboard, Length, Point, Rectangle, Shell}; - -pub use iced_native::widget::toggler::{Appearance, StyleSheet, Toggler}; - -impl<'a, Message, Renderer> Widget - for Toggler<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - 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, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - >::draw( - self, - renderer, - theme, - 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> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from(toggler: Toggler<'a, Message, Renderer>) -> Self { - Self::new(toggler) - } -} diff --git a/pure/src/widget/tooltip.rs b/pure/src/widget/tooltip.rs deleted file mode 100644 index cbc3472272..0000000000 --- a/pure/src/widget/tooltip.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Display a widget over another. -use crate::widget::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::container; -use iced_native::widget::tooltip; -use iced_native::widget::{self, Text}; -use iced_native::{Clipboard, Layout, Length, Point, Rectangle, Shell}; - -pub use iced_style::container::{Appearance, StyleSheet}; -pub use tooltip::Position; - -/// An element to display a widget over another. -#[allow(missing_debug_implementations)] -pub struct Tooltip<'a, Message, Renderer: text::Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - content: Element<'a, Message, Renderer>, - tooltip: Text, - position: Position, - gap: u16, - padding: u16, - style: ::Style, -} - -impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - /// The default padding of a [`Tooltip`] drawn by this renderer. - const DEFAULT_PADDING: u16 = 5; - - /// Creates a new [`Tooltip`]. - /// - /// [`Tooltip`]: struct.Tooltip.html - pub fn new( - content: impl Into>, - tooltip: impl ToString, - position: Position, - ) -> Self { - Tooltip { - content: content.into(), - tooltip: Text::new(tooltip.to_string()), - position, - gap: 0, - padding: Self::DEFAULT_PADDING, - style: Default::default(), - } - } - - /// Sets the size of the text of the [`Tooltip`]. - pub fn size(mut self, size: u16) -> Self { - self.tooltip = self.tooltip.size(size); - self - } - - /// Sets the font of the [`Tooltip`]. - /// - /// [`Font`]: Renderer::Font - pub fn font(mut self, font: impl Into) -> Self { - self.tooltip = self.tooltip.font(font); - self - } - - /// Sets the gap between the content and its [`Tooltip`]. - pub fn gap(mut self, gap: u16) -> Self { - self.gap = gap; - self - } - - /// Sets the padding of the [`Tooltip`]. - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; - self - } - - /// Sets the style of the [`Tooltip`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Tooltip<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - 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 width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - 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, - 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, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - ); - - let tooltip = &self.tooltip; - - tooltip::draw( - renderer, - theme, - inherited_style, - layout, - cursor_position, - viewport, - self.position, - self.gap, - self.padding, - self.style, - |renderer, limits| { - Widget::<(), Renderer>::layout(tooltip, renderer, limits) - }, - |renderer, defaults, layout, cursor_position, viewport| { - Widget::<(), Renderer>::draw( - tooltip, - &Tree::empty(), - renderer, - theme, - defaults, - layout, - cursor_position, - viewport, - ); - }, - ); - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget().overlay( - &mut tree.children[0], - layout, - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, -{ - fn from( - tooltip: Tooltip<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(tooltip) - } -} diff --git a/src/application.rs b/src/application.rs index aca97367be..58d4a57770 100644 --- a/src/application.rs +++ b/src/application.rs @@ -60,7 +60,8 @@ pub use iced_native::application::{Appearance, StyleSheet}; /// says "Hello, world!": /// /// ```no_run -/// use iced::{executor, Application, Command, Element, Settings, Text, Theme}; +/// use iced::executor; +/// use iced::{Application, Command, Element, Settings, Theme}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -86,8 +87,8 @@ pub use iced_native::application::{Appearance, StyleSheet}; /// Command::none() /// } /// -/// fn view(&mut self) -> Element { -/// Text::new("Hello, world!").into() +/// fn view(&self) -> Element { +/// "Hello, world!".into() /// } /// } /// ``` @@ -139,9 +140,7 @@ pub trait Application: Sized { /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view( - &mut self, - ) -> Element<'_, Self::Message, crate::Renderer>; + fn view(&self) -> Element<'_, Self::Message, crate::Renderer>; /// Returns the current [`Theme`] of the [`Application`]. /// @@ -249,7 +248,7 @@ where self.0.update(message) } - fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer> { + fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { self.0.view() } } diff --git a/src/lib.rs b/src/lib.rs index 4e8d67879e..100b9f771f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,15 +51,9 @@ //! We start by modelling the __state__ of our application: //! //! ``` -//! use iced::button; -//! //! struct Counter { //! // The counter value //! value: i32, -//! -//! // The local state of the two buttons -//! increment_button: button::State, -//! decrement_button: button::State, //! } //! ``` //! @@ -78,15 +72,9 @@ //! __view logic__: //! //! ``` -//! # use iced::button; -//! # //! # struct Counter { //! # // The counter value //! # value: i32, -//! # -//! # // The local state of the two buttons -//! # increment_button: button::State, -//! # decrement_button: button::State, //! # } //! # //! # #[derive(Debug, Clone, Copy)] @@ -95,28 +83,22 @@ //! # DecrementPressed, //! # } //! # -//! use iced::{Button, Column, Text}; +//! use iced::widget::{button, column, text, Column}; //! //! impl Counter { //! pub fn view(&mut self) -> Column { //! // We use a column: a simple vertical layout -//! Column::new() -//! .push( -//! // The increment button. We tell it to produce an -//! // `IncrementPressed` message when pressed -//! Button::new(&mut self.increment_button, Text::new("+")) -//! .on_press(Message::IncrementPressed), -//! ) -//! .push( -//! // We show the value of the counter here -//! Text::new(self.value.to_string()).size(50), -//! ) -//! .push( -//! // The decrement button. We tell it to produce a -//! // `DecrementPressed` message when pressed -//! Button::new(&mut self.decrement_button, Text::new("-")) -//! .on_press(Message::DecrementPressed), -//! ) +//! column![ +//! // The increment button. We tell it to produce an +//! // `IncrementPressed` message when pressed +//! button("+").on_press(Message::IncrementPressed), +//! +//! // We show the value of the counter here +//! text(self.value).size(50), +//! +//! // The decrement button. We tell it to produce a +//! button("-").on_press(Message::DecrementPressed), +//! ] //! } //! } //! ``` @@ -125,15 +107,9 @@ //! our __state__ accordingly in our __update logic__: //! //! ``` -//! # use iced::button; -//! # //! # struct Counter { //! # // The counter value //! # value: i32, -//! # -//! # // The local state of the two buttons -//! # increment_button: button::State, -//! # decrement_button: button::State, //! # } //! # //! # #[derive(Debug, Clone, Copy)] @@ -203,10 +179,6 @@ pub mod time; pub mod widget; pub mod window; -#[cfg(feature = "pure")] -#[cfg_attr(docsrs, doc(cfg(feature = "pure")))] -pub mod pure; - #[cfg(all(not(feature = "glow"), feature = "wgpu"))] use iced_winit as runtime; @@ -220,25 +192,26 @@ use iced_wgpu as renderer; use iced_glow as renderer; pub use iced_native::theme; - -#[doc(no_inline)] -pub use widget::*; +pub use runtime::event; +pub use runtime::subscription; pub use application::Application; pub use element::Element; pub use error::Error; +pub use event::Event; pub use executor::Executor; pub use renderer::Renderer; pub use result::Result; pub use sandbox::Sandbox; pub use settings::Settings; +pub use subscription::Subscription; pub use theme::Theme; pub use runtime::alignment; pub use runtime::futures; pub use runtime::{ Alignment, Background, Color, Command, ContentFit, Font, Length, Padding, - Point, Rectangle, Size, Subscription, Vector, + Point, Rectangle, Size, Vector, }; #[cfg(feature = "system")] diff --git a/src/pure.rs b/src/pure.rs deleted file mode 100644 index 23f56570a9..0000000000 --- a/src/pure.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! 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 Elm Architecture, purity, and continuity -//! As you may know, applications made with `iced` use [The Elm Architecture]. -//! -//! In a nutshell, this architecture defines the initial state of the application, a way to `view` it, and a way to `update` it after a user interaction. The `update` logic is called after a meaningful user interaction, which in turn updates the state of the application. Then, the `view` logic is executed to redisplay the application. -//! -//! Since `view` logic is only run after an `update`, all of the mutations to the application state must only happen in the `update` logic. If the application state changes anywhere else, the `view` logic will not be rerun and, therefore, the previously generated `view` may stay outdated. -//! -//! However, the `Application` trait in `iced` defines `view` as: -//! -//! ```ignore -//! pub trait Application { -//! fn view(&mut self) -> Element; -//! } -//! ``` -//! -//! As a consequence, the application state can be mutated in `view` logic. The `view` logic in `iced` is __impure__. -//! -//! This impurity is necessary because `iced` puts the burden of widget __continuity__ on its users. In other words, it's up to you to provide `iced` with the internal state of each widget every time `view` is called. -//! -//! If we take a look at the classic `counter` example: -//! -//! ```ignore -//! struct Counter { -//! value: i32, -//! increment_button: button::State, -//! decrement_button: button::State, -//! } -//! -//! // ... -//! -//! impl Counter { -//! pub fn view(&mut self) -> Column { -//! Column::new() -//! .push( -//! Button::new(&mut self.increment_button, Text::new("+")) -//! .on_press(Message::IncrementPressed), -//! ) -//! .push(Text::new(self.value.to_string()).size(50)) -//! .push( -//! Button::new(&mut self.decrement_button, Text::new("-")) -//! .on_press(Message::DecrementPressed), -//! ) -//! } -//! } -//! ``` -//! -//! We can see how we need to keep track of the `button::State` of each `Button` in our `Counter` state and provide a mutable reference to the widgets in our `view` logic. The widgets produced by `view` are __stateful__. -//! -//! While this approach forces users to keep track of widget state and causes impurity, I originally chose it because it allows `iced` to directly consume the widget tree produced by `view`. Since there is no internal state decoupled from `view` maintained by the runtime, `iced` does not need to compare (e.g. reconciliate) widget trees in order to ensure continuity. -//! -//! # Stateless widgets -//! As the library matures, the need for some kind of persistent widget data (see #553) between `view` calls becomes more apparent (e.g. incremental rendering, animations, accessibility, etc.). -//! -//! If we are going to end up having persistent widget data anyways... There is no reason to have impure, stateful widgets anymore! -//! -//! With the help of this module, we can now write a pure `counter` example: -//! -//! ```ignore -//! struct Counter { -//! value: i32, -//! } -//! -//! // ... -//! -//! impl Counter { -//! fn view(&self) -> Column { -//! Column::new() -//! .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)) -//! } -//! } -//! ``` -//! -//! Notice how we no longer need to keep track of the `button::State`! The widgets in `iced_pure` do not take any mutable application state in `view`. They are __stateless__ widgets. As a consequence, we do not need mutable access to `self` in `view` anymore. `view` becomes __pure__. -//! -//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! -//! [the original widgets]: crate::widget -//! [`button::State`]: crate::widget::button::State -//! [impure `Application`]: crate::Application -pub mod application; -pub mod widget; - -mod sandbox; - -pub use application::Application; -pub use sandbox::Sandbox; - -pub use iced_pure::helpers::*; -pub use iced_pure::Widget; -pub use iced_pure::{Pure, State}; - -/// A generic, pure [`Widget`]. -pub type Element<'a, Message, Renderer = crate::Renderer> = - iced_pure::Element<'a, Message, Renderer>; diff --git a/src/pure/application.rs b/src/pure/application.rs deleted file mode 100644 index 396854ad23..0000000000 --- a/src/pure/application.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Build interactive cross-platform applications. -use crate::pure::{self, Pure}; -use crate::window; -use crate::{Command, Executor, Settings, Subscription}; - -pub use iced_native::application::StyleSheet; - -/// 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 theme of your [`Application`]. - type Theme: Default + StyleSheet; - - /// 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 widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - ) -> pure::Element<'_, Self::Message, crate::Renderer>; - - /// Returns the current [`Theme`] of the [`Application`]. - fn theme(&self) -> Self::Theme { - Self::Theme::default() - } - - /// 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 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 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; - type Theme = A::Theme; - - 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 subscription(&self) -> Subscription { - A::subscription(&self.application) - } - - fn view( - &mut self, - ) -> crate::Element<'_, Self::Message, crate::Renderer> { - let content = A::view(&self.application); - - Pure::new(&mut self.state, content).into() - } - - fn theme(&self) -> Self::Theme { - A::theme(&self.application) - } - - fn mode(&self) -> window::Mode { - A::mode(&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 deleted file mode 100644 index a58cace768..0000000000 --- a/src/pure/sandbox.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::pure; -use crate::{Command, Error, Settings, Subscription, Theme}; - -/// A pure version of [`Sandbox`]. -/// -/// Unlike the impure version, the `view` method of this trait takes an -/// immutable reference to `self` and returns a pure [`Element`]. -/// -/// [`Sandbox`]: crate::Sandbox -/// [`Element`]: pure::Element -pub trait Sandbox { - /// The type of __messages__ your [`Sandbox`] will produce. - type Message: std::fmt::Debug + Send; - - /// Initializes the [`Sandbox`]. - /// - /// Here is where you should return the initial state of your app. - fn new() -> Self; - - /// Returns the current title of the [`Sandbox`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; - - /// Handles a __message__ and updates the state of the [`Sandbox`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by user interactions, will be handled by this method. - fn update(&mut self, message: Self::Message); - - /// Returns the widgets to display in the [`Sandbox`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> pure::Element<'_, Self::Message>; - - /// Returns the current [`Theme`] of the [`Sandbox`]. - /// - /// If you want to use your own custom theme type, you will have to use an - /// [`Application`]. - /// - /// By default, it returns [`Theme::default`]. - fn theme(&self) -> Theme { - Theme::default() - } - - /// 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; - type Theme = Theme; - - 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 view(&self) -> pure::Element<'_, T::Message> { - T::view(self) - } - - fn theme(&self) -> Self::Theme { - T::theme(self) - } - - fn subscription(&self) -> Subscription { - Subscription::none() - } - - fn scale_factor(&self) -> f64 { - T::scale_factor(self) - } - - fn should_exit(&self) -> bool { - T::should_exit(self) - } -} diff --git a/src/pure/widget.rs b/src/pure/widget.rs deleted file mode 100644 index 336f498f58..0000000000 --- a/src/pure/widget.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Pure versions of the widgets. - -/// A container that distributes its contents vertically. -pub type Column<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Column<'a, Message, Renderer>; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Row<'a, Message, 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::{Appearance, StyleSheet}; - - /// A widget that produces a message when clicked. - pub type Button<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Button<'a, Message, Renderer>; -} - -pub mod checkbox { - //! Show toggle controls using checkboxes. - pub use iced_pure::widget::checkbox::{Appearance, StyleSheet}; - - /// A box that can be checked. - pub type Checkbox<'a, Message, Renderer = crate::Renderer> = - iced_native::widget::Checkbox<'a, Message, Renderer>; -} - -pub mod container { - //! Decorate content and apply alignment. - pub use iced_pure::widget::container::{Appearance, StyleSheet}; - - /// An element decorating some content. - pub type Container<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Container<'a, Message, 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.4/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, Renderer = crate::Renderer> = - iced_pure::widget::PaneGrid<'a, Message, Renderer>; - - /// The content of a [`Pane`]. - pub type Content<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::pane_grid::Content<'a, Message, Renderer>; - - /// The title bar of a [`Pane`]. - pub type TitleBar<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::pane_grid::TitleBar<'a, Message, Renderer>; -} - -pub mod pick_list { - //! Display a dropdown list of selectable values. - pub use iced_pure::widget::pick_list::{Appearance, StyleSheet}; - - /// A widget allowing the selection of a single value from a list of options. - pub type PickList<'a, T, Message, Renderer = crate::Renderer> = - iced_pure::widget::PickList<'a, T, Message, Renderer>; -} - -pub mod radio { - //! Create choices using radio buttons. - pub use iced_pure::widget::radio::{Appearance, StyleSheet}; - - /// A circular button representing a choice. - pub type Radio = - iced_pure::widget::Radio; -} - -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, Renderer = crate::Renderer> = - iced_pure::widget::Scrollable<'a, Message, Renderer>; -} - -pub mod toggler { - //! Show toggle controls using togglers. - pub use iced_pure::widget::toggler::{Appearance, StyleSheet}; - - /// A toggler widget. - pub type Toggler<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Toggler<'a, Message, Renderer>; -} - -pub mod text_input { - //! Display fields that can be filled with text. - pub use iced_pure::widget::text_input::{Appearance, StyleSheet}; - - /// A field that can be filled with text. - pub type TextInput<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::TextInput<'a, Message, Renderer>; -} - -pub mod tooltip { - //! Display a widget over another. - pub use iced_pure::widget::tooltip::Position; - - /// A widget allowing the selection of a single value from a list of options. - pub type Tooltip<'a, Message, Renderer = crate::Renderer> = - iced_pure::widget::Tooltip<'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; -pub use tooltip::Tooltip; - -#[cfg(feature = "canvas")] -pub use iced_graphics::widget::pure::canvas; - -#[cfg(feature = "qr_code")] -pub use iced_graphics::widget::pure::qr_code; - -#[cfg(feature = "image")] -pub mod image { - //! Display images in your user interface. - pub use iced_native::image::Handle; - - /// A frame that displays an image. - pub type Image = iced_pure::widget::Image; -} - -#[cfg(feature = "svg")] -pub use iced_pure::widget::svg; - -#[cfg(feature = "canvas")] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -pub use qr_code::QRCode; - -#[cfg(feature = "image")] -pub use image::Image; - -#[cfg(feature = "svg")] -pub use svg::Svg; diff --git a/src/sandbox.rs b/src/sandbox.rs index 3ca3fe8f83..bdb6ad5a4a 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -56,7 +56,7 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// says "Hello, world!": /// /// ```no_run -/// use iced::{Element, Sandbox, Settings, Text}; +/// use iced::{Element, Sandbox, Settings}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -79,8 +79,8 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// // This application has no interactions /// } /// -/// fn view(&mut self) -> Element { -/// Text::new("Hello, world!").into() +/// fn view(&self) -> Element { +/// "Hello, world!".into() /// } /// } /// ``` @@ -108,7 +108,7 @@ pub trait Sandbox { /// Returns the widgets to display in the [`Sandbox`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message>; + fn view(&self) -> Element<'_, Self::Message>; /// Returns the current [`Theme`] of the [`Sandbox`]. /// @@ -184,7 +184,7 @@ where Command::none() } - fn view(&mut self) -> Element<'_, T::Message> { + fn view(&self) -> Element<'_, T::Message> { T::view(self) } diff --git a/src/widget.rs b/src/widget.rs index b8b5c4937b..817d2d3313 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,18 +1,7 @@ //! Display information and interactive controls in your application. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced::{button, Button}; -//! ``` -//! -//! # Stateful widgets -//! Some widgets need to keep track of __local state__. -//! -//! These widgets have their own module with a `State` type. For instance, a -//! [`TextInput`] has some [`text_input::State`]. +pub use iced_native::widget::helpers::*; + +pub use iced_native::{column, row}; /// A container that distributes its contents vertically. pub type Column<'a, Message, Renderer = crate::Renderer> = @@ -22,14 +11,18 @@ pub type Column<'a, Message, Renderer = crate::Renderer> = pub type Row<'a, Message, Renderer = crate::Renderer> = iced_native::widget::Row<'a, Message, Renderer>; -/// A paragraph of text. -pub type Text = iced_native::widget::Text; +pub mod text { + //! Write some text for your users to read. + pub use iced_native::widget::text::{Appearance, StyleSheet}; + + /// 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::{Appearance, State, StyleSheet}; + pub use iced_native::widget::button::{Appearance, StyleSheet}; /// A widget that produces a message when clicked. pub type Button<'a, Message, Renderer = crate::Renderer> = @@ -87,7 +80,7 @@ pub mod pane_grid { pub mod pick_list { //! Display a dropdown list of selectable values. - pub use iced_native::widget::pick_list::{Appearance, State, StyleSheet}; + pub use iced_native::widget::pick_list::{Appearance, StyleSheet}; /// A widget allowing the selection of a single value from a list of options. pub type PickList<'a, T, Message, Renderer = crate::Renderer> = @@ -106,7 +99,7 @@ pub mod radio { pub mod scrollable { //! Navigate an endless amount of content with a scrollbar. pub use iced_native::widget::scrollable::{ - style::Scrollbar, style::Scroller, State, StyleSheet, + snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet, }; /// A widget that can vertically display an infinite amount of content @@ -126,9 +119,9 @@ pub mod toggler { pub mod text_input { //! Display fields that can be filled with text. - //! - //! A [`TextInput`] has some local [`State`]. - pub use iced_native::widget::text_input::{Appearance, State, StyleSheet}; + pub use iced_native::widget::text_input::{ + focus, Appearance, Id, StyleSheet, + }; /// A field that can be filled with text. pub type TextInput<'a, Message, Renderer = crate::Renderer> = @@ -159,6 +152,7 @@ pub use radio::Radio; pub use rule::Rule; pub use scrollable::Scrollable; pub use slider::Slider; +pub use text::Text; pub use text_input::TextInput; pub use toggler::Toggler; pub use tooltip::Tooltip; @@ -167,6 +161,16 @@ pub use tooltip::Tooltip; #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub use iced_graphics::widget::canvas; +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +/// Creates a new [`Canvas`]. +pub fn canvas(program: P) -> Canvas +where + P: canvas::Program, +{ + Canvas::new(program) +} + #[cfg(feature = "image")] #[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub mod image { @@ -207,3 +211,22 @@ pub use qr_code::QRCode; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub use svg::Svg; + +use crate::Command; +use iced_native::widget::operation; + +/// Focuses the previous focusable widget. +pub fn focus_previous() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next() -> Command +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} diff --git a/winit/src/application.rs b/winit/src/application.rs index 99402cf564..3a5c3dac6e 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -7,6 +7,7 @@ use crate::clipboard::{self, Clipboard}; use crate::conversion; use crate::mouse; use crate::renderer; +use crate::widget::operation; use crate::{ Command, Debug, Error, Executor, Mode, Proxy, Runtime, Settings, Size, Subscription, @@ -131,9 +132,9 @@ where debug.startup_started(); let event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; @@ -146,8 +147,6 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); - let builder = settings.window.into_builder( &application.title(), application.mode(), @@ -176,20 +175,8 @@ where .expect("Append canvas to HTML body"); } - let mut clipboard = Clipboard::connect(&window); - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; - run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &window, - || compositor.fetch_information(), - ); - runtime.track(subscription); - let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::( @@ -197,10 +184,10 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, + init_command, window, settings.exit_on_close_request, )); @@ -247,10 +234,10 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, - mut clipboard: Clipboard, mut proxy: winit::event_loop::EventLoopProxy, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver>, + init_command: Command, window: winit::window::Window, exit_on_close_request: bool, ) where @@ -262,6 +249,8 @@ async fn run_instance( use iced_futures::futures::stream::StreamExt; use winit::event; + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface(&window); let mut state = State::new(&application, &window); @@ -275,9 +264,24 @@ async fn run_instance( physical_size.height, ); + run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &window, + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(build_user_interface( - &mut application, - user_interface::Cache::default(), + &application, + cache, &mut renderer, state.logical_size(), &mut debug, @@ -318,12 +322,15 @@ async fn run_instance( user_interface::State::Outdated, ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, @@ -339,7 +346,7 @@ async fn run_instance( let should_exit = application.should_exit(); user_interface = ManuallyDrop::new(build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), @@ -515,7 +522,7 @@ pub fn requests_exit( /// Builds a [`UserInterface`] for the provided [`Application`], logging /// [`struct@Debug`] information accordingly. pub fn build_user_interface<'a, A: Application>( - application: &'a mut A, + application: &'a A, cache: user_interface::Cache, renderer: &mut A::Renderer, size: Size, @@ -539,6 +546,9 @@ where /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( application: &mut A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy, @@ -556,7 +566,19 @@ pub fn update( let command = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command(command, runtime, clipboard, proxy, window, graphics_info); + run_command( + application, + cache, + state, + renderer, + command, + runtime, + clipboard, + proxy, + debug, + window, + graphics_info, + ); } let subscription = application.subscription(); @@ -564,14 +586,23 @@ pub fn update( } /// Runs the actions of a [`Command`]. -pub fn run_command( - command: Command, - runtime: &mut Runtime, Message>, +pub fn run_command( + application: &A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, + command: Command, + runtime: &mut Runtime, A::Message>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy, + debug: &mut Debug, window: &winit::window::Window, _graphics_info: impl FnOnce() -> compositor::Information + Copy, -) { +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ use iced_native::command; use iced_native::system; use iced_native::window; @@ -627,6 +658,37 @@ pub fn run_command( } } }, + command::Action::Widget(action) => { + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + ); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } } } }