diff options
Diffstat (limited to 'examples')
54 files changed, 2073 insertions, 2919 deletions
diff --git a/examples/README.md b/examples/README.md index 137d134c..bb15dc2e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,10 +27,6 @@ You can run the native version with `cargo run`: cargo run --package tour ``` -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! - -[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage - ## [Todos](todos) A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. @@ -46,7 +42,6 @@ You can run the native version with `cargo run`: ``` cargo run --package todos ``` -We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! [TodoMVC]: http://todomvc.com/ @@ -105,6 +100,7 @@ A bunch of simpler examples exist: - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. - [`scrollable`](scrollable), a showcase of the various scrollbar width options. +- [`sierpinski_triangle`](sierpinski_triangle), a [sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use `Canvas` and `Slider`. - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. - [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time. - [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. diff --git a/examples/arc/Cargo.toml b/examples/arc/Cargo.toml new file mode 100644 index 00000000..e6e74363 --- /dev/null +++ b/examples/arc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc" +version = "0.1.0" +authors = ["ThatsNoMoon <git@thatsnomoon.dev>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } diff --git a/examples/arc/README.md b/examples/arc/README.md new file mode 100644 index 00000000..303253da --- /dev/null +++ b/examples/arc/README.md @@ -0,0 +1,14 @@ +## Arc + +An application that uses the `Canvas` widget to draw a rotating arc. + +This is a simple demo for https://github.com/iced-rs/iced/pull/1358. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: +``` +cargo run --package arc +``` + +[`main`]: src/main.rs diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs new file mode 100644 index 00000000..0c619dc9 --- /dev/null +++ b/examples/arc/src/main.rs @@ -0,0 +1,126 @@ +use std::{f32::consts::PI, time::Instant}; + +use iced::executor; +use iced::widget::canvas::{ + self, Cache, Canvas, Cursor, Geometry, Path, Stroke, +}; +use iced::{ + Application, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, +}; + +pub fn main() -> iced::Result { + Arc::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct Arc { + start: Instant, + cache: Cache, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick, +} + +impl Application for Arc { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Arc { + start: Instant::now(), + cache: Default::default(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Arc - Iced") + } + + fn update(&mut self, _: Message) -> Command<Message> { + self.cache.clear(); + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + iced::time::every(std::time::Duration::from_millis(10)) + .map(|_| Message::Tick) + } + + fn view(&self) -> Element<Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +impl<Message> canvas::Program<Message> for Arc { + type State = (); + + fn draw( + &self, + _state: &Self::State, + theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { + let geometry = self.cache.draw(bounds.size(), |frame| { + let palette = theme.palette(); + + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 5.0; + + let start = Point::new(center.x, center.y - radius); + + let angle = (self.start.elapsed().as_millis() % 10_000) as f32 + / 10_000.0 + * 2.0 + * PI; + + let end = Point::new( + center.x + radius * angle.cos(), + center.y + radius * angle.sin(), + ); + + let circles = Path::new(|b| { + b.circle(start, 10.0); + b.move_to(end); + b.circle(end, 10.0); + }); + + frame.fill(&circles, palette.text); + + let path = Path::new(|b| { + b.move_to(start); + b.arc_to(center, end, 50.0); + b.line_to(end); + }); + + frame.stroke( + &path, + Stroke { + color: palette.text, + width: 10.0, + ..Stroke::default() + }, + ); + }); + + vec![geometry] + } +} diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 35b5182c..7c3916d4 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<bezier::Curve>, - button_state: button::State, } #[derive(Debug, Clone, Copy)] @@ -47,44 +45,34 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - 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<Message> { + 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, + 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<Pending>, 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<Curve> for Bezier<'a> { + type State = Option<Pending>; + 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, @@ -158,18 +149,24 @@ mod bezier { } } - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + fn draw( + &self, + state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { let content = self.state.cache.draw(bounds.size(), |frame: &mut Frame| { Curve::draw_all(self.curves, frame); 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] @@ -180,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 325ccc1a..8818fb54 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,7 +1,9 @@ +use iced::executor; +use iced::widget::canvas::{Cache, Cursor, Geometry, LineCap, Path, Stroke}; +use iced::widget::{canvas, container}; use iced::{ - canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, - executor, Application, Color, Command, Container, Element, Length, Point, - Rectangle, Settings, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, Vector, }; pub fn main() -> iced::Result { @@ -22,8 +24,9 @@ enum Message { } impl Application for Clock { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command<Message>) { @@ -65,10 +68,12 @@ impl Application for Clock { }) } - fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new(self).width(Length::Fill).height(Length::Fill); + fn view(&self) -> Element<Message> { + 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) @@ -76,8 +81,16 @@ impl Application for Clock { } } -impl canvas::Program<Message> for Clock { - fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { +impl<Message> canvas::Program<Message> for Clock { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { let clock = self.clock.draw(bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 23670b46..8fd37202 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "palette"] } -palette = "0.5.0" +palette = "0.6.0" diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md index 95a23f48..f90020b1 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 ad3004b0..42149965 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,9 +1,10 @@ -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, Hsl, Limited, Srgb}; +use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; use std::ops::RangeInclusive; @@ -49,42 +50,43 @@ impl Sandbox for ColorPalette { fn update(&mut self, message: Message) { let srgb = match message { Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), - Message::HslColorChanged(hsl) => palette::Srgb::from(hsl), - Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv), - Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb), - Message::LabColorChanged(lab) => palette::Srgb::from(lab), - Message::LchColorChanged(lch) => palette::Srgb::from(lch), + 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.clamp()); + self.theme = Theme::new(srgb); } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let base = self.theme.base; let srgb = palette::Srgb::from(base); - let hsl = palette::Hsl::from(srgb); - let hsv = palette::Hsv::from(srgb); - let hwb = palette::Hwb::from(srgb); - let lab = palette::Lab::from(srgb); - let lch = palette::Lch::from(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() + 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![ + 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() } } #[derive(Debug)] -pub struct Theme { +struct Theme { lower: Vec<Color>, base: Color, higher: Vec<Color>, @@ -98,7 +100,7 @@ impl Theme { let base = base.into(); // Convert to HSL color for manipulation - let hsl = Hsl::from(Srgb::from(base)); + let hsl = Hsl::from_color(Srgb::from(base)); let lower = [ hsl.shift_hue(-135.0).lighten(0.075), @@ -117,12 +119,12 @@ impl Theme { Theme { lower: lower .iter() - .map(|&color| Srgb::from(color).clamp().into()) + .map(|&color| Srgb::from_color(color).into()) .collect(), base, higher: higher .iter() - .map(|&color| Srgb::from(color).clamp().into()) + .map(|&color| Srgb::from_color(color).into()) .collect(), canvas_cache: canvas::Cache::default(), } @@ -139,7 +141,7 @@ impl Theme { .chain(self.higher.iter()) } - pub fn view(&mut self) -> Element<Message> { + pub fn view(&self) -> Element<Message> { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -207,14 +209,14 @@ impl Theme { text.vertical_alignment = alignment::Vertical::Bottom; - let hsl = Hsl::from(Srgb::from(self.base)); + 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(graded.clamp()).into(); + let color: Color = Srgb::from_color(graded).into(); let anchor = Point { x: (i as f32) * box_size.width, @@ -235,8 +237,16 @@ impl Theme { } } -impl canvas::Program<Message> for Theme { - fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { +impl<Message> canvas::Program<Message> for Theme { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &iced::Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { let theme = self.canvas_cache.draw(bounds.size(), |frame| { self.draw(frame); }); @@ -262,7 +272,6 @@ fn color_hex_string(color: &Color) -> String { #[derive(Default)] struct ColorPicker<C: ColorSpace> { - sliders: [slider::State; 3], color_space: PhantomData<C>, } @@ -277,37 +286,30 @@ trait ColorSpace: Sized { fn to_string(&self) -> String; } -impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { - fn view(&mut self, color: C) -> Element<C> { +impl<C: ColorSpace + Copy> ColorPicker<C> { + fn view(&self, color: C) -> Element<C> { let [c1, c2, c3] = color.components(); - let [s1, s2, s3] = &mut self.sliders; let [cr1, cr2, cr3] = C::COMPONENT_RANGES; - fn slider<C: Clone>( - state: &mut slider::State, + fn slider<'a, C: Clone>( range: RangeInclusive<f64>, component: f32, - update: impl Fn(f32) -> C + 'static, - ) -> Slider<f64, C> { - 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 39335cf1..06b1e53a 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<u32>, } @@ -35,39 +36,31 @@ impl Sandbox for Component { } } - fn view(&mut self) -> Element<Message> { - Container::new(NumericInput::new( - &mut self.numeric_input, - self.value, - Message::NumericInputChanged, - )) - .padding(20) - .height(Length::Fill) - .center_y() - .into() + fn view(&self) -> Element<Message> { + container(numeric_input(self.value, Message::NumericInputChanged)) + .padding(20) + .height(Length::Fill) + .center_y() + .into() } } mod numeric_input { - use iced_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::{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<Message> { value: Option<u32>, on_change: Box<dyn Fn(Option<u32>) -> Message>, } - #[derive(Default)] - pub struct State { - input: text_input::State, - decrement_button: button::State, - increment_button: button::State, + pub fn numeric_input<Message>( + value: Option<u32>, + on_change: impl Fn(Option<u32>) -> Message + 'static, + ) -> NumericInput<Message> { + NumericInput::new(value, on_change) } #[derive(Debug, Clone)] @@ -77,28 +70,33 @@ mod numeric_input { DecrementPressed, } - impl<'a, Message> NumericInput<'a, Message> { + impl<Message> NumericInput<Message> { pub fn new( - state: &'a mut State, value: Option<u32>, on_change: impl Fn(Option<u32>) -> Message + 'static, ) -> Self { Self { - state, value, on_change: Box::new(on_change), } } } - impl<'a, Message, Renderer> Component<Message, Renderer> - for NumericInput<'a, Message> + impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message> where - Renderer: 'a + text::Renderer, + 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<Message> { + fn update( + &mut self, + _state: &mut Self::State, + event: Event, + ) -> Option<Message> { match event { Event::IncrementPressed => Some((self.on_change)(Some( self.value.unwrap_or_default().saturating_add(1), @@ -120,11 +118,10 @@ mod numeric_input { } } - fn view(&mut self) -> Element<Event, Renderer> { - let button = |state, label, on_press| { - Button::new( - state, - Text::new(label) + fn view(&self, _state: &Self::State) -> Element<Event, Renderer> { + let button = |label, on_press| { + button( + text(label) .width(Length::Fill) .height(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center) @@ -134,47 +131,37 @@ 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() .map(u32::to_string) - .as_ref() - .map(String::as_str) + .as_deref() .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<NumericInput<'a, Message>> + impl<'a, Message, Renderer> From<NumericInput<Message>> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: text::Renderer + 'a, + 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<Message>) -> Self { + iced_lazy::component(numeric_input) } } } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 931cf5e1..13dcbf86 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,16 +1,12 @@ -use iced::{ - button, Alignment, Button, 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)] @@ -23,7 +19,7 @@ impl Sandbox for Counter { type Message = Message; fn new() -> Self { - Self::default() + Self { value: 0 } } fn title(&self) -> String { @@ -41,19 +37,14 @@ impl Sandbox for Counter { } } - fn view(&mut self) -> Element<Message> { - 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<Message> { + 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 28edf256..c37a1a12 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<Message, Renderer> Widget<Message, Renderer> for Circle where Renderer: renderer::Renderer, @@ -45,7 +50,9 @@ mod circle { fn draw( &self, + _state: &widget::Tree, renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -63,21 +70,19 @@ mod circle { } } - impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Circle + impl<'a, Message, Renderer> From<Circle> for Element<'a, Message, Renderer> where Renderer: renderer::Renderer, { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) + fn from(circle: Circle) -> Self { + Self::new(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()) @@ -85,7 +90,6 @@ pub fn main() -> iced::Result { struct Example { radius: f32, - slider: slider::State, } #[derive(Debug, Clone, Copy)] @@ -97,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 { @@ -115,25 +116,18 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - 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<Message> { + 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/download.rs b/examples/download_progress/src/download.rs index 7db1206b..39dd843f 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -70,9 +70,7 @@ async fn download<I: Copy>( // We do not let the stream die, as it would start a // new download repeatedly if the user is not careful // in case of errors. - let _: () = iced::futures::future::pending().await; - - unreachable!() + iced::futures::future::pending().await } } } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 21804a0a..3ef9ef7a 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,6 +1,8 @@ +use iced::executor; +use iced::widget::{button, column, container, progress_bar, text, Column}; use iced::{ - button, executor, Alignment, Application, Button, Column, Command, - Container, Element, Length, ProgressBar, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; mod download; @@ -13,7 +15,6 @@ pub fn main() -> iced::Result { struct Example { downloads: Vec<Download>, last_id: usize, - add: button::State, } #[derive(Debug, Clone)] @@ -24,8 +25,9 @@ pub enum Message { } impl Application for Example { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Example, Command<Message>) { @@ -33,7 +35,6 @@ impl Application for Example { Example { downloads: vec![Download::new(0)], last_id: 0, - add: button::State::new(), }, Command::none(), ) @@ -46,7 +47,7 @@ impl Application for Example { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Add => { - self.last_id = self.last_id + 1; + self.last_id += 1; self.downloads.push(Download::new(self.last_id)); } @@ -71,21 +72,19 @@ impl Application for Example { Subscription::batch(self.downloads.iter().map(Download::subscription)) } - fn view(&mut self) -> Element<Message> { - 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<Message> { + 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() @@ -103,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, } } @@ -131,8 +128,8 @@ impl Download { } pub fn progress(&mut self, new_progress: download::Progress) { - match &mut self.state { - State::Downloading { progress } => match new_progress { + if let State::Downloading { progress } = &mut self.state { + match new_progress { download::Progress::Started => { *progress = 0.0; } @@ -140,17 +137,12 @@ 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; } - }, - _ => {} + } } } @@ -164,7 +156,7 @@ impl Download { } } - pub fn view(&mut self) -> Element<Message> { + pub fn view(&self) -> Element<Message> { let current_progress = match &self.state { State::Idle { .. } => 0.0, State::Downloading { progress } => *progress, @@ -172,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 7f024c56..234e1423 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,6 +1,9 @@ +use iced::alignment; +use iced::executor; +use iced::widget::{button, checkbox, container, text, Column}; use iced::{ - alignment, button, executor, Alignment, Application, Button, Checkbox, - Column, Command, Container, Element, Length, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use iced_native::{window, Event}; @@ -15,7 +18,6 @@ pub fn main() -> iced::Result { struct Events { last: Vec<iced_native::Event>, enabled: bool, - exit: button::State, should_exit: bool, } @@ -27,8 +29,9 @@ enum Message { } impl Application for Events { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Events, Command<Message>) { @@ -72,23 +75,23 @@ impl Application for Events { self.should_exit } - fn view(&mut self) -> Element<Message> { - let events = self.last.iter().fold( - Column::new().spacing(10), - |column, event| { - column.push(Text::new(format!("{:?}", event)).size(40)) - }, + fn view(&self) -> Element<Message> { + 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), ) @@ -103,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 c45a8205..5d518d2f 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<Message> { + fn view(&self) -> Element<Message> { 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 ab8b80e4..a2030275 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,20 +1,20 @@ //! This example showcases an interactive version of the Game of Life, invented //! by John Conway. It leverages a `Canvas` together with other widgets. mod preset; -mod style; use grid::Grid; -use iced::button::{self, Button}; +use 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, Container, 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, @@ -55,6 +54,7 @@ enum Message { impl Application for GameOfLife { type Message = Message; + type Theme = Theme; type Executor = executor::Default; type Flags = (); @@ -131,39 +131,87 @@ impl Application for GameOfLife { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { 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(), ); - let content = 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::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) - .style(style::Container) .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![ + 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, 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<Vector>), Ticked { result: Result<Life, TickError>, 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<'a>(&'a mut self) -> Element<'a, Message> { + pub fn view(&self) -> Element<Message> { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -328,15 +392,18 @@ mod grid { } } - impl<'a> canvas::Program<Message> for Grid { + impl canvas::Program<Message> for Grid { + type State = Interaction; + fn update( - &mut self, + &self, + interaction: &mut Interaction, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option<Message>) { 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), @@ -445,7 +517,13 @@ mod grid { } } - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + fn draw( + &self, + _interaction: &Interaction, + _theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); let life = self.life_cache.draw(bounds.size(), |frame| { @@ -571,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, @@ -803,90 +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<Preset>, -} -impl Controls { - fn view<'a>( - &'a mut self, - is_playing: bool, - is_grid_enabled: bool, - speed: usize, - preset: Preset, - ) -> Element<'a, Message> { - 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(style::Button), - ) - .push( - Button::new(&mut self.next_button, Text::new("Next")) - .on_press(Message::Next) - .style(style::Button), - ); - - 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, - ) - .style(style::Slider), - ) - .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) - .style(style::PickList), - ) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Clear), - ) - .into() + impl Default for Interaction { + fn default() -> Self { + Self::None + } } } diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs index 05157b6a..964b9120 100644 --- a/examples/game_of_life/src/preset.rs +++ b/examples/game_of_life/src/preset.rs @@ -1,7 +1,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Preset { Custom, - XKCD, + Xkcd, Glider, SmallExploder, Exploder, @@ -14,7 +14,7 @@ pub enum Preset { pub static ALL: &[Preset] = &[ Preset::Custom, - Preset::XKCD, + Preset::Xkcd, Preset::Glider, Preset::SmallExploder, Preset::Exploder, @@ -30,7 +30,7 @@ impl Preset { #[rustfmt::skip] let cells = match self { Preset::Custom => vec![], - Preset::XKCD => vec![ + Preset::Xkcd => vec![ " xxx ", " x x ", " x x ", @@ -116,7 +116,7 @@ impl Preset { impl Default for Preset { fn default() -> Preset { - Preset::XKCD + Preset::Xkcd } } @@ -127,7 +127,7 @@ impl std::fmt::Display for Preset { "{}", match self { Preset::Custom => "Custom", - Preset::XKCD => "xkcd #2293", + Preset::Xkcd => "xkcd #2293", Preset::Glider => "Glider", Preset::SmallExploder => "Small Exploder", Preset::Exploder => "Exploder", diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs deleted file mode 100644 index be9a0e96..00000000 --- a/examples/game_of_life/src/style.rs +++ /dev/null @@ -1,189 +0,0 @@ -use iced::{button, container, pick_list, slider, Background, Color}; - -const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, -); - -const DESTRUCTIVE: Color = Color::from_rgb( - 0xC0 as f32 / 255.0, - 0x47 as f32 / 255.0, - 0x47 as f32 / 255.0, -); - -const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, -); - -const BACKGROUND: Color = Color::from_rgb( - 0x2F as f32 / 255.0, - 0x31 as f32 / 255.0, - 0x36 as f32 / 255.0, -); - -pub struct Container; - -impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(Color::from_rgb8( - 0x36, 0x39, 0x3F, - ))), - text_color: Some(Color::WHITE), - ..container::Style::default() - } - } -} - -pub struct Button; - -impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(HOVERED)), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } -} - -pub struct Clear; - -impl button::StyleSheet for Clear { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(DESTRUCTIVE)), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(Color { - a: 0.5, - ..DESTRUCTIVE - })), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } -} - -pub struct Slider; - -impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9.0 }, - color: ACTIVE, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } -} - -pub struct PickList; - -impl pick_list::StyleSheet for PickList { - fn menu(&self) -> pick_list::Menu { - pick_list::Menu { - text_color: Color::WHITE, - background: BACKGROUND.into(), - border_width: 1.0, - border_color: Color { - a: 0.7, - ..Color::BLACK - }, - selected_background: Color { - a: 0.5, - ..Color::BLACK - } - .into(), - selected_text_color: Color::WHITE, - } - } - - fn active(&self) -> pick_list::Style { - pick_list::Style { - text_color: Color::WHITE, - background: BACKGROUND.into(), - border_width: 1.0, - border_color: Color { - a: 0.6, - ..Color::BLACK - }, - border_radius: 2.0, - icon_size: 0.5, - ..pick_list::Style::default() - } - } - - fn hovered(&self) -> pick_list::Style { - let active = self.active(); - - pick_list::Style { - border_color: Color { - a: 0.9, - ..Color::BLACK - }, - ..active - } - } -} diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 58dfa3ad..d8b99ab3 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -13,10 +13,12 @@ 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)] pub struct Rainbow; impl Rainbow { @@ -25,7 +27,11 @@ mod rainbow { } } - impl<Message, B> Widget<Message, Renderer<B>> for Rainbow + pub fn rainbow() -> Rainbow { + Rainbow + } + + impl<Message, B, T> Widget<Message, Renderer<B, T>> for Rainbow where B: Backend, { @@ -39,7 +45,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -49,7 +55,9 @@ mod rainbow { fn draw( &self, - renderer: &mut Renderer<B>, + _tree: &widget::Tree, + renderer: &mut Renderer<B, T>, + _theme: &T, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -147,37 +155,31 @@ mod rainbow { } } - impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow + impl<'a, Message, B, T> From<Rainbow> for Element<'a, Message, Renderer<B, T>> where B: Backend, { - fn into(self) -> Element<'a, Message, Renderer<B>> { - Element::new(self) + fn from(rainbow: Rainbow) -> Self { + Self::new(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 { @@ -186,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 f387b4e5..076d37d3 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<Message, Renderer> { - let [r, g, b] = &mut self.sliders; + fn view(&self) -> Element<Message, Renderer> { 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 @@ -89,13 +86,13 @@ impl Program for Controls { .spacing(10) .push( Text::new("Background color") - .color(Color::WHITE), + .style(Color::WHITE), ) .push(sliders) .push( Text::new(format!("{:?}", background_color)) .size(14) - .color(Color::WHITE), + .style(Color::WHITE), ), ), ) diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index 1007b90f..f161c8a0 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -12,7 +12,8 @@ use iced_glow::glow; use iced_glow::{Backend, Renderer, Settings, Viewport}; use iced_glutin::conversion; use iced_glutin::glutin; -use iced_glutin::{program, Clipboard, Debug, Size}; +use iced_glutin::renderer; +use iced_glutin::{program, Clipboard, Color, Debug, Size}; pub fn main() { env_logger::init(); @@ -57,7 +58,7 @@ pub fn main() { let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(&windowed_context.window()); + let mut clipboard = Clipboard::connect(windowed_context.window()); let mut renderer = Renderer::new(Backend::new(&gl, Settings::default())); @@ -72,13 +73,12 @@ pub fn main() { ); let mut resized = false; - let scene = Scene::new(&gl, &shader_version); + let scene = Scene::new(&gl, shader_version); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { - Event::LoopDestroyed => return, Event::WindowEvent { event, .. } => { match event { WindowEvent::CursorMoved { position, .. } => { @@ -125,6 +125,10 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, + &iced_glow::Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, + }, &mut clipboard, &mut debug, ); diff --git a/examples/integration_wgpu/README.md b/examples/integration_wgpu/README.md index faefa153..ece9ba1e 100644 --- a/examples/integration_wgpu/README.md +++ b/examples/integration_wgpu/README.md @@ -12,7 +12,7 @@ The __[`main`]__ file contains all the code of the example. You can run it with `cargo run`: ``` -cargo run --package integration +cargo run --package integration_wgpu ``` ### How to run this example with WebGL backend diff --git a/examples/integration_wgpu/src/controls.rs b/examples/integration_wgpu/src/controls.rs index 9bca40eb..6c41738c 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<Message, Renderer> { - let [r, g, b] = &mut self.sliders; - let t = &mut self.text_input; + fn view(&self) -> Element<Message, Renderer> { 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 @@ -100,19 +92,18 @@ impl Program for Controls { .spacing(10) .push( Text::new("Background color") - .color(Color::WHITE), + .style(Color::WHITE), ) .push(sliders) .push( Text::new(format!("{:?}", background_color)) .size(14) - .color(Color::WHITE), + .style(Color::WHITE), ) - .push(TextInput::new( - t, + .push(text_input( "Placeholder", text, - move |text| Message::TextChanged(text), + Message::TextChanged, )), ), ) diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration_wgpu/src/main.rs index 045ee0d3..86a0d6a4 100644 --- a/examples/integration_wgpu/src/main.rs +++ b/examples/integration_wgpu/src/main.rs @@ -5,9 +5,11 @@ use controls::Controls; use scene::Scene; use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{conversion, futures, program, winit, Clipboard, Debug, Size}; +use iced_winit::{ + conversion, futures, program, renderer, winit, Clipboard, Color, Debug, + Size, +}; -use futures::task::SpawnExt; use winit::{ dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, @@ -71,7 +73,7 @@ pub fn main() { let instance = wgpu::Instance::new(backend); let surface = unsafe { instance.create_surface(&window) }; - let (format, (mut device, queue)) = futures::executor::block_on(async { + let (format, (device, queue)) = futures::executor::block_on(async { let adapter = wgpu::util::initialize_adapter_from_env_or_default( &instance, backend, @@ -91,7 +93,9 @@ pub fn main() { ( surface - .get_preferred_format(&adapter) + .get_supported_formats(&adapter) + .first() + .copied() .expect("Get preferred format"), adapter .request_device( @@ -114,24 +118,23 @@ pub fn main() { format, width: physical_size.width, height: physical_size.height, - present_mode: wgpu::PresentMode::Mailbox, + present_mode: wgpu::PresentMode::AutoVsync, }, ); let mut resized = false; - // Initialize staging belt and local pool + // Initialize staging belt let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); - let mut local_pool = futures::executor::LocalPool::new(); // Initialize scene and GUI controls - let scene = Scene::new(&mut device, format); + let scene = Scene::new(&device, format); let controls = Controls::new(); // Initialize iced let mut debug = Debug::new(); let mut renderer = - Renderer::new(Backend::new(&mut device, Settings::default(), format)); + Renderer::new(Backend::new(&device, Settings::default(), format)); let mut state = program::State::new( controls, @@ -188,6 +191,8 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, + &iced_wgpu::Theme::Dark, + &renderer::Style { text_color: Color::WHITE }, &mut clipboard, &mut debug, ); @@ -203,11 +208,11 @@ pub fn main() { surface.configure( &device, &wgpu::SurfaceConfiguration { + format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: format, width: size.width, height: size.height, - present_mode: wgpu::PresentMode::Mailbox, + present_mode: wgpu::PresentMode::AutoVsync, }, ); @@ -239,7 +244,7 @@ pub fn main() { // And then iced on top renderer.with_primitives(|backend, primitive| { backend.present( - &mut device, + &device, &mut staging_belt, &mut encoder, &view, @@ -262,12 +267,8 @@ pub fn main() { ); // And recall staging buffers - local_pool - .spawner() - .spawn(staging_belt.recall()) - .expect("Recall staging buffers"); + staging_belt.recall(); - local_pool.run_until_stalled(); } Err(error) => match error { wgpu::SurfaceError::OutOfMemory => { diff --git a/examples/integration_wgpu/src/scene.rs b/examples/integration_wgpu/src/scene.rs index fbda1326..3e41fbda 100644 --- a/examples/integration_wgpu/src/scene.rs +++ b/examples/integration_wgpu/src/scene.rs @@ -23,7 +23,7 @@ impl Scene { ) -> wgpu::RenderPass<'a> { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, - color_attachments: &[wgpu::RenderPassColorAttachment { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, ops: wgpu::Operations { @@ -39,7 +39,7 @@ impl Scene { }), store: true, }, - }], + })], depth_stencil_attachment: None, }) } @@ -55,8 +55,8 @@ fn build_pipeline( texture_format: wgpu::TextureFormat, ) -> wgpu::RenderPipeline { let (vs_module, fs_module) = ( - device.create_shader_module(&wgpu::include_wgsl!("shader/vert.wgsl")), - device.create_shader_module(&wgpu::include_wgsl!("shader/frag.wgsl")), + device.create_shader_module(wgpu::include_wgsl!("shader/vert.wgsl")), + device.create_shader_module(wgpu::include_wgsl!("shader/frag.wgsl")), ); let pipeline_layout = @@ -66,40 +66,37 @@ fn build_pipeline( bind_group_layouts: &[], }); - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", - buffers: &[], - }, - fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", - targets: &[wgpu::ColorTargetState { - format: texture_format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - }], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Ccw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - - pipeline + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs_module, + entry_point: "main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fs_module, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Ccw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }) } diff --git a/examples/integration_wgpu/src/shader/frag.wgsl b/examples/integration_wgpu/src/shader/frag.wgsl index a6f61336..cf27bb56 100644 --- a/examples/integration_wgpu/src/shader/frag.wgsl +++ b/examples/integration_wgpu/src/shader/frag.wgsl @@ -1,4 +1,4 @@ -[[stage(fragment)]] -fn main() -> [[location(0)]] vec4<f32> { +@fragment +fn main() -> @location(0) vec4<f32> { return vec4<f32>(1.0, 0.0, 0.0, 1.0); } diff --git a/examples/integration_wgpu/src/shader/vert.wgsl b/examples/integration_wgpu/src/shader/vert.wgsl index 7ef47fb2..e353e6ba 100644 --- a/examples/integration_wgpu/src/shader/vert.wgsl +++ b/examples/integration_wgpu/src/shader/vert.wgsl @@ -1,5 +1,5 @@ -[[stage(vertex)]] -fn main([[builtin(vertex_index)]] in_vertex_index: u32) -> [[builtin(position)]] vec4<f32> { +@vertex +fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> { let x = f32(1 - i32(in_vertex_index)) * 0.5; let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5; return vec4<f32>(x, y, 0.0, 1.0); diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 2962ca25..ae8fa22b 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,14 +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 { @@ -36,6 +35,7 @@ enum Message { impl Application for Example { type Message = Message; + type Theme = Theme; type Executor = executor::Default; type Flags = (); @@ -153,57 +153,47 @@ impl Application for Example { }) } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { 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(style::Button::Pin) - .padding(3); - - let title = Row::with_children(vec![ - pin_button.into(), - Text::new("Pane").into(), - Text::new(content.id.to_string()) - .color(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::TitleBar::Focused + style::title_bar_focused } else { - style::TitleBar::Active + 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 { - style::Pane::Focused + style::pane_focused } else { - style::Pane::Active + style::pane_active }) }) .width(Length::Fill) @@ -213,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) @@ -253,247 +243,132 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { } 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<Message> { - let Content { - scroll, - split_horizontally, - split_vertically, - close, - .. - } = self; - - let button = |state, label, message, style| { - Button::new( - state, - Text::new(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - .style(style) - }; - - let mut controls = Column::new() - .spacing(5) - .max_width(150) - .push(button( - split_horizontally, - "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), - style::Button::Primary, - )) - .push(button( - split_vertically, - "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), - style::Button::Primary, - )); - - if total_panes > 1 && !is_pinned { - controls = controls.push(button( - close, - "Close", - Message::Close(pane), - style::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<Message> { - let mut button = - Button::new(&mut self.close, Text::new("Close").size(14)) - .style(style::Button::Control) - .padding(3); - if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); - } - button.into() - } + button.into() } mod style { - use crate::PANE_ID_COLOR_FOCUSED; - use iced::{button, container, Background, Color, Vector}; - - const SURFACE: Color = Color::from_rgb( - 0xF2 as f32 / 255.0, - 0xF3 as f32 / 255.0, - 0xF5 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub enum TitleBar { - Active, - Focused, - } + use iced::widget::container; + use iced::Theme; - impl container::StyleSheet for TitleBar { - fn style(&self) -> container::Style { - let pane = match self { - Self::Active => Pane::Active, - Self::Focused => Pane::Focused, - } - .style(); + pub fn title_bar_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - container::Style { - text_color: Some(Color::WHITE), - background: Some(pane.border_color.into()), - ..Default::default() - } + container::Appearance { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() } } - pub enum Pane { - Active, - Focused, - } + pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - impl container::StyleSheet for Pane { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(SURFACE)), - border_width: 2.0, - border_color: match self { - Self::Active => Color::from_rgb(0.7, 0.7, 0.7), - Self::Focused => Color::BLACK, - }, - ..Default::default() - } + container::Appearance { + text_color: Some(palette.primary.strong.text), + background: Some(palette.primary.strong.color.into()), + ..Default::default() } } - pub enum Button { - Primary, - Destructive, - Control, - Pin, - } + pub fn pane_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - let (background, text_color) = match self { - Button::Primary => (Some(ACTIVE), Color::WHITE), - Button::Destructive => { - (None, Color::from_rgb8(0xFF, 0x47, 0x47)) - } - Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE), - Button::Pin => (Some(ACTIVE), Color::WHITE), - }; - - button::Style { - text_color, - background: background.map(Background::Color), - border_radius: 5.0, - shadow_offset: Vector::new(0.0, 0.0), - ..button::Style::default() - } + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.background.strong.color, + ..Default::default() } + } - fn hovered(&self) -> button::Style { - let active = self.active(); + pub fn pane_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - let background = match self { - Button::Primary => Some(HOVERED), - Button::Destructive => Some(Color { - a: 0.2, - ..active.text_color - }), - Button::Control => Some(PANE_ID_COLOR_FOCUSED), - Button::Pin => Some(HOVERED), - }; - - button::Style { - background: background.map(Background::Color), - ..active - } + 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/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 52303d70..9df1f5c7 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<Language>, selected_language: Option<Language>, } @@ -38,26 +34,25 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - let pick_list = PickList::new( - &mut self.pick_list, + fn view(&self) -> Element<Message> { + 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 85c26987..4fe2d07c 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,7 @@ +use iced::futures; +use iced::widget::{self, column, container, image, row, text}; use iced::{ - button, futures, image, Alignment, Application, Button, Column, Command, - Container, Element, Length, Row, Settings, Text, + Alignment, Application, Color, Command, Element, Length, Settings, Theme, }; pub fn main() -> iced::Result { @@ -10,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)] @@ -26,8 +22,9 @@ enum Message { } impl Application for Pokedex { - type Executor = iced::executor::Default; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Pokedex, Command<Message>) { @@ -50,17 +47,12 @@ impl Application for Pokedex { fn update(&mut self, message: Message) -> Command<Message> { 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() } @@ -75,27 +67,28 @@ impl Application for Pokedex { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { 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() @@ -110,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<Message> { - 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) - .color([0.5, 0.5, 0.5]), - ), - ) - .push(Text::new(&self.description)), - ) - .into() + fn view(&self) -> Element<Message> { + 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<Pokemon, Error> { @@ -188,8 +170,7 @@ impl Pokemon { let description = entry .flavor_text_entries .iter() - .filter(|text| text.language.name == "en") - .next() + .find(|text| text.language.name == "en") .ok_or(Error::LanguageError)?; Ok(Pokemon { @@ -201,7 +182,6 @@ impl Pokemon { .map(|c| if c.is_control() { ' ' } else { c }) .collect(), image, - image_viewer: image::viewer::State::new(), }) } @@ -237,30 +217,6 @@ impl From<reqwest::Error> for Error { } } -fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { - Button::new(state, Text::new(text)) - .padding(10) - .style(style::Button::Primary) -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } +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 c9a8e798..d4ebe4d3 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<Message> { - 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<Message> { + 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/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 92c82d45..6f487e4c 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,8 +1,6 @@ -use iced::qr_code::{self, QRCode}; -use iced::text_input::{self, TextInput}; -use iced::{ - Alignment, 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()) @@ -11,7 +9,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct QRGenerator { data: String, - input: text_input::State, qr_code: Option<qr_code::State>, } @@ -45,13 +42,12 @@ impl Sandbox for QRGenerator { } } - fn view(&mut self) -> Element<Message> { - let title = Text::new("QR Code Generator") + fn view(&self) -> Element<Message> { + let title = text("QR Code Generator") .size(70) - .color([0.5, 0.5, 0.5]); + .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, @@ -59,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 8e027504..b7b3dedc 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,176 +1,182 @@ -mod style; - -use iced::{ - button, scrollable, Button, Column, Container, Element, Length, - ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text, +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()) } struct ScrollableDemo { - theme: style::Theme, + theme: Theme, variants: Vec<Variant>, } #[derive(Debug, Clone)] enum Message { - ThemeChanged(style::Theme), + ThemeChanged(Theme), ScrollToTop(usize), ScrollToBottom(usize), 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<Message>) { + ( + 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<Message> { 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<Message> { + fn view(&self) -> Element<Message> { let ScrollableDemo { theme, variants, .. } = self; - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + column!["Choose a theme:"].spacing(10), |column, option| { - column.push( - Radio::new( - *option, - format!("{:?}", option), - Some(*theme), - Message::ThemeChanged, - ) - .style(*theme), - ) + column.push(radio( + format!("{:?}", option), + *option, + Some(*theme), + Message::ThemeChanged, + )) }, ); 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) - }) - .style(*theme) - .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( - Container::new(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .style(*theme), - ) - .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(), ) @@ -178,29 +184,27 @@ 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).style(self.theme)) - .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() .center_y() - .style(self.theme) .into() } + + fn theme(&self) -> Theme { + self.theme + } } /// 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<u16>, scrollbar_margin: Option<u16>, scroller_width: Option<u16>, @@ -212,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, @@ -222,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), @@ -232,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), @@ -242,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), @@ -252,4 +244,8 @@ impl Variant { }, ] } + + pub fn id(i: usize) -> scrollable::Id { + scrollable::Id::new(format!("scrollable-{}", i)) + } } diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs deleted file mode 100644 index 0ed38b00..00000000 --- a/examples/scrollable/src/style.rs +++ /dev/null @@ -1,191 +0,0 @@ -use iced::{container, radio, rule, scrollable}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Theme { - Light, - Dark, -} - -impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; -} - -impl Default for Theme { - fn default() -> Theme { - Theme::Light - } -} - -impl<'a> From<Theme> for Box<dyn container::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } -} - -impl<'a> From<Theme> for Box<dyn radio::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } -} - -impl<'a> From<Theme> for Box<dyn scrollable::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } -} - -impl From<Theme> for Box<dyn rule::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Rule.into(), - } - } -} - -mod dark { - use iced::{container, radio, rule, scrollable, Color}; - - const BACKGROUND: Color = Color::from_rgb( - 0x36 as f32 / 255.0, - 0x39 as f32 / 255.0, - 0x3F as f32 / 255.0, - ); - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const SCROLLBAR: Color = Color::from_rgb( - 0x2E as f32 / 255.0, - 0x33 as f32 / 255.0, - 0x38 as f32 / 255.0, - ); - - const SCROLLER: Color = Color::from_rgb( - 0x20 as f32 / 255.0, - 0x22 as f32 / 255.0, - 0x25 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Color { - a: 0.99, - ..BACKGROUND - } - .into(), - text_color: Color::WHITE.into(), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: SURFACE.into(), - dot_color: ACTIVE, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Color { a: 0.5, ..SURFACE }.into(), - ..self.active() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: Color { - a: 0.8, - ..SCROLLBAR - } - .into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: Color { a: 0.7, ..SCROLLER }, - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: SCROLLBAR.into(), - scroller: scrollable::Scroller { - color: SCROLLER, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: ACCENT, - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Rule; - - impl rule::StyleSheet for Rule { - fn style(&self) -> rule::Style { - rule::Style { - color: SURFACE, - width: 2, - radius: 1.0, - fill_mode: rule::FillMode::Percent(30.0), - } - } - } -} diff --git a/examples/sierpinski_triangle/Cargo.toml b/examples/sierpinski_triangle/Cargo.toml new file mode 100644 index 00000000..39d45f64 --- /dev/null +++ b/examples/sierpinski_triangle/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sierpinski_triangle" +version = "0.1.0" +authors = ["xkenmon <xkenmon@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "debug"] } +rand = "0.8.4" diff --git a/examples/sierpinski_triangle/README.md b/examples/sierpinski_triangle/README.md new file mode 100644 index 00000000..9fd18257 --- /dev/null +++ b/examples/sierpinski_triangle/README.md @@ -0,0 +1,16 @@ +## Sierpinski Triangle Emulator + +A simple [Sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use canvas and slider. + +Left-click add fixed point, right-click remove fixed point. + +<div align="center"> + <a href="https://gfycat.com/flippantrectangularechidna"> + <img src="https://thumbs.gfycat.com/FlippantRectangularEchidna-size_restricted.gif"> + </a> +</div> + +You can run with cargo: +``` +cargo run --package sierpinski_triangle +```
\ No newline at end of file diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs new file mode 100644 index 00000000..1d25d171 --- /dev/null +++ b/examples/sierpinski_triangle/src/main.rs @@ -0,0 +1,193 @@ +use std::fmt::Debug; + +use iced::executor; +use iced::widget::canvas::event::{self, Event}; +use iced::widget::canvas::{self, Canvas}; +use iced::widget::{column, row, slider, text}; +use iced::{ + Application, Color, Command, Length, Point, Rectangle, Settings, Size, + Theme, +}; + +use rand::Rng; + +fn main() -> iced::Result { + SierpinskiEmulator::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Debug)] +struct SierpinskiEmulator { + graph: SierpinskiGraph, +} + +#[derive(Debug, Clone)] +pub enum Message { + IterationSet(i32), + PointAdded(Point), + PointRemoved, +} + +impl Application for SierpinskiEmulator { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, iced::Command<Self::Message>) { + let emulator = SierpinskiEmulator { + graph: SierpinskiGraph::new(), + }; + (emulator, Command::none()) + } + + fn title(&self) -> String { + "Sierpinski Triangle Emulator".to_string() + } + + fn update( + &mut self, + message: Self::Message, + ) -> iced::Command<Self::Message> { + match message { + Message::IterationSet(cur_iter) => { + self.graph.iteration = cur_iter; + } + Message::PointAdded(point) => { + self.graph.fix_points.push(point); + self.graph.random_points.clear(); + } + Message::PointRemoved => { + self.graph.fix_points.pop(); + self.graph.random_points.clear(); + } + } + + self.graph.redraw(); + + Command::none() + } + + fn view(&self) -> iced::Element<'_, Self::Message> { + column![ + Canvas::new(&self.graph) + .width(Length::Fill) + .height(Length::Fill), + row![ + text(format!("Iteration: {:?}", self.graph.iteration)), + slider(0..=10000, self.graph.iteration, Message::IterationSet) + .width(Length::Fill) + ] + .padding(10) + .spacing(20), + ] + .width(Length::Fill) + .align_items(iced::Alignment::Center) + .into() + } +} + +#[derive(Default, Debug)] +struct SierpinskiGraph { + iteration: i32, + fix_points: Vec<Point>, + random_points: Vec<Point>, + cache: canvas::Cache, +} + +impl canvas::Program<Message> for SierpinskiGraph { + type State = (); + + fn update( + &self, + _state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: canvas::Cursor, + ) -> (event::Status, Option<Message>) { + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; + + match event { + Event::Mouse(mouse_event) => { + let message = match mouse_event { + iced::mouse::Event::ButtonPressed( + iced::mouse::Button::Left, + ) => Some(Message::PointAdded(cursor_position)), + iced::mouse::Event::ButtonPressed( + iced::mouse::Button::Right, + ) => Some(Message::PointRemoved), + _ => None, + }; + (event::Status::Captured, message) + } + _ => (event::Status::Ignored, None), + } + } + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> Vec<canvas::Geometry> { + let geom = self.cache.draw(bounds.size(), |frame| { + frame.stroke( + &canvas::Path::rectangle(Point::ORIGIN, frame.size()), + canvas::Stroke::default(), + ); + + if self.fix_points.is_empty() { + return; + } + + let mut last = None; + + for _ in 0..self.iteration { + let p = self.gen_rand_point(last); + let path = canvas::Path::rectangle(p, Size::new(1_f32, 1_f32)); + + frame.stroke(&path, canvas::Stroke::default()); + + last = Some(p); + } + + self.fix_points.iter().for_each(|p| { + let path = canvas::Path::circle(*p, 5.0); + frame.fill(&path, Color::from_rgb8(0x12, 0x93, 0xD8)); + }); + }); + + vec![geom] + } +} + +impl SierpinskiGraph { + fn new() -> SierpinskiGraph { + SierpinskiGraph::default() + } + + fn redraw(&mut self) { + self.cache.clear(); + } + + fn gen_rand_point(&self, last: Option<Point>) -> Point { + let dest_point_idx = + rand::thread_rng().gen_range(0..self.fix_points.len()); + + let dest_point = self.fix_points[dest_point_idx]; + let cur_point = last.or_else(|| Some(self.fix_points[0])).unwrap(); + + Point::new( + (dest_point.x + cur_point.x) / 2_f32, + (dest_point.y + cur_point.y) / 2_f32, + ) + } +} diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 12184dd1..c59d73a8 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -6,10 +6,16 @@ //! Inspired by the example found in the MDN docs[1]. //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system +use iced::application; +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::{ - canvas::{self, Cursor, Path, Stroke}, - executor, time, window, 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; @@ -31,8 +37,9 @@ enum Message { } impl Application for SolarSystem { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command<Message>) { @@ -59,16 +66,26 @@ impl Application for SolarSystem { } fn subscription(&self) -> Subscription<Message> { - time::every(std::time::Duration::from_millis(10)) - .map(|instant| Message::Tick(instant)) + time::every(std::time::Duration::from_millis(10)).map(Message::Tick) } - fn view(&mut self) -> Element<Message> { - Canvas::new(&mut self.state) + fn view(&self) -> Element<Message> { + canvas(&self.state) .width(Length::Fill) .height(Length::Fill) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } + + fn style(&self) -> theme::Application { + theme::Application::Custom(|_theme| application::Appearance { + background_color: Color::BLACK, + text_color: Color::WHITE, + }) + } } #[derive(Debug)] @@ -129,24 +146,24 @@ impl State { } impl<Message> canvas::Program<Message> for State { + type State = (); + fn draw( &self, + _state: &Self::State, + _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; let background = self.space_cache.draw(bounds.size(), |frame| { - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let stars = Path::new(|path| { for (p, size) in &self.stars { path.rectangle(*p, Size::new(*size, *size)); } }); - frame.fill(&space, Color::BLACK); - frame.translate(frame.center() - Point::ORIGIN); frame.fill(&stars, Color::WHITE); }); diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index dc8a4de7..b8cee807 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,7 +1,12 @@ +use iced::alignment; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::time; +use iced::widget::{button, column, container, row, text}; use iced::{ - alignment, button, executor, time, Alignment, Application, Button, Column, - Command, Container, Element, Length, Row, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, }; + use std::time::{Duration, Instant}; pub fn main() -> iced::Result { @@ -11,8 +16,6 @@ pub fn main() -> iced::Result { struct Stopwatch { duration: Duration, state: State, - toggle: button::State, - reset: button::State, } enum State { @@ -28,8 +31,9 @@ enum Message { } impl Application for Stopwatch { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Stopwatch, Command<Message>) { @@ -37,8 +41,6 @@ impl Application for Stopwatch { Stopwatch { duration: Duration::default(), state: State::Idle, - toggle: button::State::new(), - reset: button::State::new(), }, Command::none(), ) @@ -60,13 +62,12 @@ impl Application for Stopwatch { self.state = State::Idle; } }, - Message::Tick(now) => match &mut self.state { - State::Ticking { last_tick } => { + Message::Tick(now) => { + if let State::Ticking { last_tick } = &mut self.state { self.duration += now - *last_tick; *last_tick = now; } - _ => {} - }, + } Message::Reset => { self.duration = Duration::default(); } @@ -84,13 +85,13 @@ impl Application for Stopwatch { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { 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, @@ -99,42 +100,34 @@ impl Application for Stopwatch { )) .size(40); - let button = |state, label, style| { - Button::new( - state, - Text::new(label) - .horizontal_alignment(alignment::Horizontal::Center), + let button = |label| { + button( + text(label).horizontal_alignment(alignment::Horizontal::Center), ) - .min_width(80) .padding(10) - .style(style) + .width(Length::Units(80)) }; let toggle_button = { - let (label, color) = match self.state { - State::Idle => ("Start", style::Button::Primary), - State::Ticking { .. } => ("Stop", style::Button::Destructive), + let label = match self.state { + State::Idle => "Start", + State::Ticking { .. } => "Stop", }; - button(&mut self.toggle, label, color).on_press(Message::Toggle) + button(label).on_press(Message::Toggle) }; - let reset_button = - button(&mut self.reset, "Reset", style::Button::Secondary) - .on_press(Message::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() @@ -142,29 +135,3 @@ impl Application for Stopwatch { .into() } } - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index b4ef3e87..cda53e87 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,8 +1,9 @@ -use iced::{ - button, scrollable, slider, text_input, Alignment, Button, Checkbox, - Column, Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox, - Scrollable, Settings, Slider, Space, Text, TextInput, 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()) @@ -10,12 +11,8 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Styling { - theme: style::Theme, - scroll: scrollable::State, - input: text_input::State, + theme: Theme, input_value: String, - button: button::State, - slider: slider::State, slider_value: f32, checkbox_value: bool, toggler_value: bool, @@ -23,7 +20,7 @@ struct Styling { #[derive(Debug, Clone)] enum Message { - ThemeChanged(style::Theme), + ThemeChanged(Theme), InputChanged(String), ButtonPressed, SliderChanged(f32), @@ -53,541 +50,88 @@ impl Sandbox for Styling { } } - fn view(&mut self) -> Element<Message> { - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + fn view(&self) -> Element<Message> { + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + column![text("Choose a theme:")].spacing(10), |column, theme| { - column.push( - Radio::new( - *theme, - format!("{:?}", theme), - Some(self.theme), - Message::ThemeChanged, - ) - .style(self.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, ) .padding(10) - .size(20) - .style(self.theme); + .size(20); - let button = Button::new(&mut self.button, Text::new("Submit")) + let button = button("Submit") .padding(10) - .on_press(Message::ButtonPressed) - .style(self.theme); + .on_press(Message::ButtonPressed); - let slider = Slider::new( - &mut self.slider, - 0.0..=100.0, - self.slider_value, - Message::SliderChanged, - ) - .style(self.theme); + let slider = + slider(0.0..=100.0, self.slider_value, Message::SliderChanged); - let progress_bar = - ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); + 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)) - .style(self.theme) - .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, - ) - .style(self.theme); + ); - let toggler = Toggler::new( - self.toggler_value, + let toggler = toggler( String::from("Toggle me!"), + self.toggler_value, Message::TogglerToggled, ) .width(Length::Shrink) - .spacing(10) - .style(self.theme); - - let content = Column::new() - .spacing(20) - .padding(20) - .max_width(600) - .push(choose_theme) - .push(Rule::horizontal(38).style(self.theme)) - .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).style(self.theme)) - .push( - Column::new() - .width(Length::Shrink) - .spacing(20) - .push(checkbox) - .push(toggler), - ), - ); + .spacing(10); + + 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::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .style(self.theme) .into() } -} - -mod style { - use iced::{ - button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, toggler, - }; - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - Light, - Dark, - } - - impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; - } - - impl Default for Theme { - fn default() -> Theme { - Theme::Light - } - } - - impl<'a> From<Theme> for Box<dyn container::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn radio::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn text_input::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::TextInput.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn button::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => light::Button.into(), - Theme::Dark => dark::Button.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn scrollable::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn slider::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Slider.into(), - } - } - } - - impl From<Theme> for Box<dyn progress_bar::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::ProgressBar.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn checkbox::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Checkbox.into(), - } - } - } - - impl From<Theme> for Box<dyn toggler::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Toggler.into(), - } - } - } - - impl From<Theme> for Box<dyn rule::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Rule.into(), - } - } - } - - mod light { - use iced::{button, Color, Vector}; - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Color::from_rgb(0.11, 0.42, 0.87).into(), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } - } - mod dark { - use iced::{ - button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, toggler, Color, - }; - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Color::from_rgb8(0x36, 0x39, 0x3F).into(), - text_color: Color::WHITE.into(), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: SURFACE.into(), - dot_color: ACTIVE, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Color { a: 0.5, ..SURFACE }.into(), - ..self.active() - } - } - } - - pub struct TextInput; - - impl text_input::StyleSheet for TextInput { - fn active(&self) -> text_input::Style { - text_input::Style { - background: SURFACE.into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - - fn focused(&self) -> text_input::Style { - text_input::Style { - border_width: 1.0, - border_color: ACCENT, - ..self.active() - } - } - - fn hovered(&self) -> text_input::Style { - text_input::Style { - border_width: 1.0, - border_color: Color { a: 0.3, ..ACCENT }, - ..self.focused() - } - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.4, 0.4, 0.4) - } - - fn value_color(&self) -> Color { - Color::WHITE - } - - fn selection_color(&self) -> Color { - ACTIVE - } - } - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: ACTIVE.into(), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: HOVERED.into(), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: SURFACE.into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: ACTIVE, - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: Color { a: 0.5, ..SURFACE }.into(), - scroller: scrollable::Scroller { - color: HOVERED, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Slider; - - impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9.0 }, - color: ACTIVE, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } - } - - pub struct ProgressBar; - - impl progress_bar::StyleSheet for ProgressBar { - fn style(&self) -> progress_bar::Style { - progress_bar::Style { - background: SURFACE.into(), - bar: ACTIVE.into(), - border_radius: 10.0, - } - } - } - - pub struct Checkbox; - - impl checkbox::StyleSheet for Checkbox { - fn active(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: if is_checked { ACTIVE } else { SURFACE } - .into(), - checkmark_color: Color::WHITE, - border_radius: 2.0, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Color { - a: 0.8, - ..if is_checked { ACTIVE } else { SURFACE } - } - .into(), - ..self.active(is_checked) - } - } - } - - pub struct Toggler; - - impl toggler::StyleSheet for Toggler { - fn active(&self, is_active: bool) -> toggler::Style { - toggler::Style { - background: if is_active { ACTIVE } else { SURFACE }, - background_border: None, - foreground: if is_active { Color::WHITE } else { ACTIVE }, - foreground_border: None, - } - } - - fn hovered(&self, is_active: bool) -> toggler::Style { - toggler::Style { - background: if is_active { ACTIVE } else { SURFACE }, - background_border: None, - foreground: if is_active { - Color { - a: 0.5, - ..Color::WHITE - } - } else { - Color { a: 0.5, ..ACTIVE } - }, - foreground_border: None, - } - } - } - - pub struct Rule; - - impl rule::StyleSheet for Rule { - fn style(&self) -> rule::Style { - rule::Style { - color: SURFACE, - width: 2, - radius: 1.0, - fill_mode: rule::FillMode::Padded(15), - } - } - } + fn theme(&self) -> Theme { + self.theme } } diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 8707fa3b..27d175da 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/Cargo.toml b/examples/system_information/Cargo.toml new file mode 100644 index 00000000..7d1e4b94 --- /dev/null +++ b/examples/system_information/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "system_information" +version = "0.1.0" +authors = ["Richard <richardsoncusto@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["system"] } +bytesize = { version = "1.1.0" } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs new file mode 100644 index 00000000..af67742f --- /dev/null +++ b/examples/system_information/src/main.rs @@ -0,0 +1,149 @@ +use iced::widget::{button, column, container, text}; +use iced::{ + executor, system, Application, Command, Element, Length, Settings, Theme, +}; + +use bytesize::ByteSize; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +enum Example { + Loading, + Loaded { information: system::Information }, +} + +#[derive(Clone, Debug)] +enum Message { + InformationReceived(system::Information), + Refresh, +} + +impl Application for Example { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self::Loading, + system::fetch_information(Message::InformationReceived), + ) + } + + fn title(&self) -> String { + String::from("System Information - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Refresh => { + *self = Self::Loading; + + return system::fetch_information(Message::InformationReceived); + } + Message::InformationReceived(information) => { + *self = Self::Loaded { information }; + } + } + + Command::none() + } + + fn view(&self) -> Element<Message> { + 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 + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let system_kernel = text(format!( + "System kernel: {}", + information + .system_kernel + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let system_version = text(format!( + "System version: {}", + information + .system_version + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let cpu_brand = + text(format!("Processor brand: {}", information.cpu_brand)); + + let cpu_cores = text(format!( + "Processor cores: {}", + information + .cpu_cores + .map_or("unknown".to_string(), |cores| cores + .to_string()) + )); + + let memory_readable = + ByteSize::kb(information.memory_total).to_string(); + + let memory_total = text(format!( + "Memory (total): {} kb ({})", + information.memory_total, memory_readable + )); + + let memory_text = if let Some(memory_used) = + information.memory_used + { + let memory_readable = ByteSize::kb(memory_used).to_string(); + + format!("{} kb ({})", memory_used, memory_readable) + } else { + String::from("None") + }; + + let memory_used = + text(format!("Memory (used): {}", memory_text)); + + let graphics_adapter = text(format!( + "Graphics adapter: {}", + information.graphics_adapter + )); + + let graphics_backend = text(format!( + "Graphics backend: {}", + information.graphics_backend + )); + + 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(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into() + } +} diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5781ddef..2326ffc6 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" @@ -20,6 +21,6 @@ wasm-timer = "0.2" [package.metadata.deb] assets = [ - ["target/release/todos", "usr/bin/iced-todos", "755"], + ["target/release-opt/todos", "usr/bin/iced-todos", "755"], ["iced-todos.desktop", "usr/share/applications/", "644"], ] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 0b889407..bddc0e71 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,15 +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::{ - Application, Checkbox, Column, Command, Container, Element, Font, Length, - Row, Settings, Text, +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; +use iced::theme::{self, Theme}; +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)] @@ -20,12 +36,9 @@ enum Todos { #[derive(Debug, Default)] struct State { - scroll: scrollable::State, - input: text_input::State, input_value: String, filter: Filter, tasks: Vec<Task>, - controls: Controls, dirty: bool, saving: bool, } @@ -38,11 +51,13 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, } impl Application for Todos { - type Executor = iced::executor::Default; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Todos, Command<Message>) { @@ -79,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() { @@ -95,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; @@ -133,54 +176,57 @@ impl Application for Todos { ) } else { Command::none() - } + }; + + Command::batch(vec![command, save]) } } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { 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) - .color([0.5, 0.5, 0.5]) + .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...", @@ -191,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<Message> { + 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)] @@ -221,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 } } @@ -248,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, } } @@ -264,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<TaskMessage> { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, + fn view(&self, i: usize) -> Element<TaskMessage> { + 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(style::Button::Icon), - ) - .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(style::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, -} +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row<Message> { - 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 { - style::Button::FilterSelected - } else { - style::Button::FilterActive - }); + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); - button.on_press(Message::FilterChanged(filter)).padding(8) - }; - - 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)] @@ -434,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), ) @@ -445,13 +449,13 @@ 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) - .color([0.7, 0.7, 0.7]), + .style(Color::from([0.7, 0.7, 0.7])), ) .width(Length::Fill) .height(Length::Units(200)) @@ -462,22 +466,22 @@ 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()) +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) .font(ICONS) .width(Length::Units(20)) .horizontal_alignment(alignment::Horizontal::Center) .size(20) } -fn edit_icon() -> Text { +fn edit_icon() -> Text<'static> { icon('\u{F303}') } -fn delete_icon() -> Text { +fn delete_icon() -> Text<'static> { icon('\u{F1F8}') } @@ -491,15 +495,15 @@ struct SavedState { #[derive(Debug, Clone)] enum LoadError { - FileError, - FormatError, + File, + Format, } #[derive(Debug, Clone)] enum SaveError { - FileError, - WriteError, - FormatError, + File, + Write, + Format, } #[cfg(not(target_arch = "wasm32"))] @@ -510,7 +514,7 @@ impl SavedState { { project_dirs.data_dir().into() } else { - std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + std::env::current_dir().unwrap_or_default() }; path.push("todos.json"); @@ -525,37 +529,37 @@ impl SavedState { let mut file = async_std::fs::File::open(Self::path()) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; file.read_to_string(&mut contents) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + 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::FormatError)?; + .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::FileError)?; + .map_err(|_| SaveError::File)?; } { let mut file = async_std::fs::File::create(path) .await - .map_err(|_| SaveError::FileError)?; + .map_err(|_| SaveError::File)?; file.write_all(json.as_bytes()) .await - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; } // This is a simple way to save at most once every couple seconds @@ -574,82 +578,28 @@ impl SavedState { } async fn load() -> Result<SavedState, LoadError> { - let storage = Self::storage().ok_or(LoadError::FileError)?; + let storage = Self::storage().ok_or(LoadError::File)?; let contents = storage .get_item("state") - .map_err(|_| LoadError::FileError)? - .ok_or(LoadError::FileError)?; + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + serde_json::from_str(&contents).map_err(|_| LoadError::Format) } async fn save(self) -> Result<(), SaveError> { - let storage = Self::storage().ok_or(SaveError::FileError)?; + let storage = Self::storage().ok_or(SaveError::File)?; let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; + .map_err(|_| SaveError::Format)?; storage .set_item("state", &json) - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; Ok(()) } } - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - FilterActive, - FilterSelected, - Icon, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - match self { - Button::FilterActive => button::Style::default(), - Button::FilterSelected => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.2, 0.2, 0.7, - ))), - border_radius: 10.0, - text_color: Color::WHITE, - ..button::Style::default() - }, - Button::Icon => button::Style { - text_color: Color::from_rgb(0.5, 0.5, 0.5), - ..button::Style::default() - }, - Button::Destructive => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.8, 0.2, 0.2, - ))), - border_radius: 5.0, - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 1.0), - ..button::Style::default() - }, - } - } - - fn hovered(&self) -> button::Style { - let active = self.active(); - - button::Style { - text_color: match self { - Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), - Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7), - _ => active.text_color, - }, - shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), - ..active - } - } - } -} diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index cfeaf6a6..35b862a8 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -1,138 +1,75 @@ -use iced::tooltip::{self, Tooltip}; -use iced::{ - alignment, button, Alignment, Button, Column, Container, Element, Length, - Row, Sandbox, Settings, Text, -}; +use iced::theme; +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<Message> { - 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.into(), - bottom.into(), - left.into(), - right.into(), - ]) - .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.into(), - ]) - .width(Length::Fill) - .height(Length::Fill) - .spacing(50); + fn view(&self) -> Element<Message> { + 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(style::Tooltip) - .into() -} - -mod style { - use iced::container; - use iced::Color; - - pub struct Tooltip; - - impl container::StyleSheet for Tooltip { - fn style(&self) -> container::Style { - container::Style { - text_color: Some(Color::from_rgb8(0xEE, 0xEE, 0xEE)), - background: Some(Color::from_rgb(0.11, 0.42, 0.87).into()), - border_radius: 12.0, - ..container::Style::default() - } - } +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/README.md b/examples/tour/README.md index e7cd2d5c..731e7e66 100644 --- a/examples/tour/README.md +++ b/examples/tour/README.md @@ -23,6 +23,11 @@ You can run the native version with `cargo run`: cargo run --package tour ``` -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! +The web version can be run with [`trunk`]: -[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage +``` +cd examples/tour +trunk serve +``` + +[`trunk`]: https://trunkrs.dev/ diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index e199c88c..378508e1 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,8 +1,11 @@ -use iced::{ - alignment, button, scrollable, slider, text_input, Button, Checkbox, Color, - Column, Container, ContentFit, Element, Image, Length, Radio, Row, Sandbox, - Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, +use iced::alignment; +use iced::theme; +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(); @@ -12,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, } @@ -24,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, } } @@ -49,56 +46,49 @@ impl Sandbox for Tour { } } - fn view(&mut self) -> Element<Message> { - let Tour { - steps, - scroll, - back_button, - next_button, - .. - } = self; + fn view(&self) -> Element<Message> { + 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(style::Button::Secondary), + .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(style::Button::Primary), + .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 scrollable = Scrollable::new(scroll) - .push(Container::new(content).width(Length::Fill).center_x()); + let content: Element<_> = column![ + steps.view(self.debug).map(Message::StepMessage), + controls, + ] + .max_width(540) + .spacing(20) + .padding(20) + .into(); + + let scrollable = scrollable( + container(if self.debug { + content.explain(Color::BLACK) + } else { + content + }) + .width(Length::Fill) + .center_x(), + ); - Container::new(scrollable) - .height(Length::Fill) - .center_y() - .into() + container(scrollable).height(Length::Fill).center_y().into() } } @@ -119,35 +109,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::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, @@ -160,7 +139,7 @@ impl Steps { self.steps[self.current].update(msg, debug); } - fn view(&mut self, debug: bool) -> Element<StepMessage> { + fn view(&self, debug: bool) -> Element<StepMessage> { self.steps[self.current].view(debug) } @@ -192,38 +171,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<Language>, - }, - 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<Language> }, + 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, } @@ -236,8 +191,7 @@ pub enum StepMessage { TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageHeightChanged(u16), - ImageFitSelected(ContentFit), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -282,14 +236,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) => { @@ -342,34 +291,21 @@ impl<'a> Step { } } - fn view(&mut self, debug: bool) -> Element<StepMessage> { + fn view(&self, debug: bool) -> Element<StepMessage> { match self { Step::Welcome => Self::welcome(), Step::Radio { selection } => Self::radio(*selection), Step::Toggler { can_continue } => Self::toggler(*can_continue), - Step::Slider { 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(), } @@ -377,59 +313,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), ) @@ -437,257 +365,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)).color(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<Language>) -> 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 @@ -699,79 +568,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!") - .color([0.7, 0.7, 0.7]) + 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) - .min_width(100) + .width(Length::Units(100)) } -fn color_slider( - state: &mut slider::State, +fn color_slider<'a>( component: f32, - update: impl Fn(f32) -> Color + 'static, -) -> Slider<f64, StepMessage> { - 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) @@ -818,36 +670,3 @@ pub enum Layout { Row, Column, } - -mod style { - use iced::button; - use iced::{Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } -} diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index ee2d249a..3257b519 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,6 +1,7 @@ +use iced::executor; +use iced::widget::{container, text}; use iced::{ - executor, Application, Command, Container, Element, Length, Settings, - Subscription, Text, + Application, Command, Element, Length, Settings, Subscription, Theme, }; use iced_native::{ event::{MacOS, PlatformSpecific}, @@ -22,8 +23,9 @@ enum Message { } impl Application for App { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (App, Command<Message>) { @@ -53,13 +55,13 @@ impl Application for App { iced_native::subscription::events().map(Message::EventOccurred) } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let content = match &self.url { - Some(url) => Text::new(format!("{}", 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 db131dd7..3981f699 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "websocket" -version = "0.1.0" +version = "1.0.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" publish = false @@ -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 13596ddd..ae65e064 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<Event> { struct Connect; @@ -32,7 +33,7 @@ pub fn connect() -> Subscription<Event> { ) } Err(_) => { - let _ = tokio::time::sleep( + tokio::time::sleep( tokio::time::Duration::from_secs(1), ) .await; @@ -63,7 +64,7 @@ pub fn connect() -> Subscription<Event> { } 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)) @@ -79,6 +80,7 @@ pub fn connect() -> Subscription<Event> { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum State { Disconnected, Connected( @@ -101,8 +103,7 @@ pub struct Connection(mpsc::Sender<Message>); impl Connection { pub fn send(&mut self, message: Message) { - let _ = self - .0 + self.0 .try_send(message) .expect("Send message to echo server"); } @@ -133,14 +134,14 @@ impl Message { } } -impl From<Message> 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/echo/server.rs b/examples/websocket/src/echo/server.rs index 7702d417..fef89a12 100644 --- a/examples/websocket/src/echo/server.rs +++ b/examples/websocket/src/echo/server.rs @@ -27,9 +27,9 @@ use warp::Filter; // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. pub async fn run() { - let routes = warp::path::end().and(warp::ws()).map(|ws: warp::ws::Ws| { - ws.on_upgrade(move |socket| user_connected(socket)) - }); + let routes = warp::path::end() + .and(warp::ws()) + .map(|ws: warp::ws::Ws| ws.on_upgrade(user_connected)); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } @@ -40,7 +40,7 @@ async fn user_connected(ws: WebSocket) { tokio::task::spawn(async move { while let Some(message) = rx.next().await { - let _ = user_ws_tx.send(message).await.unwrap_or_else(|e| { + user_ws_tx.send(message).await.unwrap_or_else(|e| { eprintln!("websocket send error: {}", e); }); } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index c03a9f3a..3902e04c 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, + 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<echo::Message>, - message_log: scrollable::State, new_message: String, - new_message_state: text_input::State, - new_message_button: button::State, state: State, } @@ -34,6 +30,7 @@ enum Message { impl Application for WebSocket { type Message = Message; + type Theme = Theme; type Flags = (); type Executor = executor::Default; @@ -52,46 +49,53 @@ 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<Message> { echo::connect().map(Message::Echo) } - fn view(&mut self) -> Element<Message> { - let message_log = if self.messages.is_empty() { - Container::new( - Text::new("Your messages will appear here...") - .color(Color::from_rgb8(0x88, 0x88, 0x88)), + fn view(&self) -> Element<Message> { + 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) .height(Length::Fill) @@ -99,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), ) @@ -136,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) @@ -160,3 +164,7 @@ impl Default for State { Self::Disconnected } } + +lazy_static::lazy_static! { + static ref MESSAGE_LOG: scrollable::Id = scrollable::Id::unique(); +} |