diff options
Diffstat (limited to 'examples')
57 files changed, 3945 insertions, 1054 deletions
diff --git a/examples/README.md b/examples/README.md index 04399b93..32ccf724 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T [TodoMVC]: http://todomvc.com/ +## [Game of Life](game_of_life) +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file. + +<div align="center"> + <a href="https://gfycat.com/briefaccurateaardvark"> + <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway + ## [Styling](styling) An example showcasing custom styling with a light and dark theme. @@ -69,15 +90,20 @@ cargo run --package styling ## Extras A bunch of simpler examples exist: -- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`]. +- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget. - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. +- [`color_palette`](color_palette), a color palette generator based on a user-defined root color. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). - [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. +- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. - [`events`](events), a log of native events displayed using a conditional `Subscription`. - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). - [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application. +- [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized. +- [`pick_list`](pick_list), a dropdown list of selectable options. - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. +- [`scrollable`](scrollable), a showcase of the various scrollbar width options. - [`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/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml index b13a0aa5..a88975a7 100644 --- a/examples/bezier_tool/Cargo.toml +++ b/examples/bezier_tool/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } -lyon = "0.15" +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md index 933f2120..ebbb12cc 100644 --- a/examples/bezier_tool/README.md +++ b/examples/bezier_tool/README.md @@ -1,6 +1,6 @@ ## Bézier tool -A Paint-like tool for drawing Bézier curves using [`lyon`]. +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. The __[`main`]__ file contains all the code of the example. @@ -16,4 +16,3 @@ cargo run --package bezier_tool ``` [`main`]: src/main.rs -[`lyon`]: https://github.com/nical/lyon diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index c3fbf276..97832e01 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,294 +1,13 @@ -//! This example showcases a simple native custom widget that renders arbitrary -//! path with `lyon`. -mod bezier { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - input, layout, Clipboard, Color, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector, - VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box<dyn Fn(Curve) -> Message>, - } - - #[derive(Debug, Clone, Copy)] - pub struct Curve { - from: Point, - to: Point, - control: Point, - } - - #[derive(Default)] - pub struct State { - pending: Option<Pending>, - } - - enum Pending { - One { from: Point }, - Two { from: Point, to: Point }, - } - - impl<'a, Message> Bezier<'a, Message> { - pub fn new<F>( - state: &'a mut State, - curves: &'a [Curve], - on_click: F, - ) -> Self - where - F: 'static + Fn(Curve) -> Message, - { - Self { - state, - curves, - on_click: Box::new(on_click), - } - } - } - - impl<'a, Message> Widget<Message, Renderer> for Bezier<'a, Message> { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits - .height(Length::Fill) - .width(Length::Fill) - .resolve(Size::ZERO); - layout::Node::new(size) - } - - fn draw( - &self, - _renderer: &mut Renderer, - defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let mut buffer: VertexBuffers<Vertex2D, u32> = VertexBuffers::new(); - let mut path_builder = lyon::path::Path::builder(); - - let bounds = layout.bounds(); - - // Draw rectangle border with lyon. - basic_shapes::stroke_rectangle( - &lyon::math::Rect::new( - lyon::math::Point::new(0.5, 0.5), - lyon::math::Size::new( - bounds.width - 1.0, - bounds.height - 1.0, - ), - ), - &StrokeOptions::default().with_line_width(1.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - }, - ), - ) - .unwrap(); - - for curve in self.curves { - path_builder.move_to(lyon::math::Point::new( - curve.from.x, - curve.from.y, - )); - - path_builder.quadratic_bezier_to( - lyon::math::Point::new(curve.control.x, curve.control.y), - lyon::math::Point::new(curve.to.x, curve.to.y), - ); - } - - match self.state.pending { - None => {} - Some(Pending::One { from }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.line_to(lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - )); - } - Some(Pending::Two { from, to }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ), - lyon::math::Point::new(to.x, to.y), - ); - } - } - - let mut tessellator = StrokeTessellator::new(); - - // Draw strokes with lyon. - tessellator - .tessellate( - &path_builder.build(), - &StrokeOptions::default().with_line_width(3.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| { - Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - } - }, - ), - ) - .unwrap(); - - let mesh = Primitive::Mesh2D { - origin: Point::new(bounds.x, bounds.y), - buffers: Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - }; - - ( - Primitive::Clip { - bounds, - offset: Vector::new(0, 0), - content: Box::new( - if self.curves.is_empty() - && self.state.pending.is_none() - { - let instructions = Primitive::Text { - bounds, - color: Color { - a: defaults.text.color.a * 0.7, - ..defaults.text.color - }, - content: String::from( - "Click to create bezier curves!", - ), - font: Font::Default, - size: 30.0, - horizontal_alignment: - HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - Primitive::Group { - primitives: vec![mesh, instructions], - } - } else { - mesh - }, - ), - }, - MouseCursor::OutOfBounds, - ) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match event { - Event::Mouse(input::mouse::Event::Input { - state: input::ButtonState::Pressed, - .. - }) => { - let new_point = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - match self.state.pending { - None => { - self.state.pending = - Some(Pending::One { from: new_point }); - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: new_point, - }); - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - messages.push((self.on_click)(Curve { - from, - to, - control: new_point, - })); - } - } - } - _ => {} - } - } - } - } - - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Bezier<'a, Message> - where - Message: 'static, - { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use bezier::Bezier; +//! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::{ - button, Align, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, + button, Align, Button, Column, Element, Length, Sandbox, Settings, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings { antialiasing: true, ..Settings::default() - }); + }) } #[derive(Default)] @@ -319,6 +38,7 @@ impl Sandbox for Example { match message { Message::AddCurve(curve) => { self.curves.push(curve); + self.bezier.request_redraw(); } Message::Clear => { self.bezier = bezier::State::default(); @@ -328,7 +48,7 @@ impl Sandbox for Example { } fn view(&mut self) -> Element<Message> { - let content = Column::new() + Column::new() .padding(20) .spacing(20) .align_items(Align::Center) @@ -337,22 +57,189 @@ impl Sandbox for Example { .width(Length::Shrink) .size(50), ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) + .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() + } +} + +mod bezier { + use iced::{ + canvas::event::{self, Event}, + canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke}, + mouse, Element, Length, Point, Rectangle, + }; + + #[derive(Default)] + pub struct State { + pending: Option<Pending>, + cache: canvas::Cache, + } - Container::new(content) + impl State { + pub fn view<'a>( + &'a mut self, + curves: &'a [Curve], + ) -> Element<'a, Curve> { + Canvas::new(Bezier { + state: self, + curves, + }) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() .into() + } + + pub fn request_redraw(&mut self) { + self.cache.clear() + } + } + + struct Bezier<'a> { + state: &'a mut State, + curves: &'a [Curve], + } + + impl<'a> canvas::Program<Curve> for Bezier<'a> { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option<Curve>) { + 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 { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: cursor_position, + }); + + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: cursor_position, + }) + } + } + } + _ => None, + }; + + (event::Status::Captured, message) + } + _ => (event::Status::Ignored, None), + } + } + + fn draw(&self, 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(), + ); + }); + + if let Some(pending) = &self.state.pending { + let pending_curve = pending.draw(bounds, cursor); + + vec![content, pending_curve] + } else { + vec![content] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + if cursor.is_over(&bounds) { + mouse::Interaction::Crosshair + } else { + mouse::Interaction::default() + } + } + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + impl Curve { + fn draw_all(curves: &[Curve], frame: &mut Frame) { + let curves = Path::new(|p| { + for curve in curves { + p.move_to(curve.from); + p.quadratic_curve_to(curve.control, curve.to); + } + }); + + frame.stroke(&curves, Stroke::default().with_width(2.0)); + } + } + + #[derive(Debug, Clone, Copy)] + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl Pending { + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { + let mut frame = Frame::new(bounds.size()); + + if let Some(cursor_position) = cursor.position_in(&bounds) { + match *self { + Pending::One { from } => { + let line = Path::line(from, cursor_position); + frame.stroke(&line, Stroke::default().with_width(2.0)); + } + Pending::Two { from, to } => { + let curve = Curve { + from, + to, + control: cursor_position, + }; + + Curve::draw_all(&[curve], &mut frame); + } + }; + } + + frame.into_geometry() + } } } diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 308cbfbb..c6e32379 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } chrono = "0.4" -async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index d8266f06..b317ac00 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,9 +1,10 @@ use iced::{ - canvas, executor, Application, Canvas, Color, Command, Container, Element, - Length, Point, Settings, Subscription, Vector, + canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, + executor, time, Application, Color, Command, Container, Element, Length, + Point, Rectangle, Settings, Subscription, Vector, }; -pub fn main() { +pub fn main() -> iced::Result { Clock::run(Settings { antialiasing: true, ..Settings::default() @@ -11,8 +12,8 @@ pub fn main() { } struct Clock { - now: LocalTime, - clock: canvas::layer::Cache<LocalTime>, + now: chrono::DateTime<chrono::Local>, + clock: Cache, } #[derive(Debug, Clone, Copy)] @@ -23,12 +24,13 @@ enum Message { impl Application for Clock { type Executor = executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Self, Command<Message>) { + fn new(_flags: ()) -> (Self, Command<Message>) { ( Clock { - now: chrono::Local::now().into(), - clock: canvas::layer::Cache::new(), + now: chrono::Local::now(), + clock: Default::default(), }, Command::none(), ) @@ -41,7 +43,7 @@ impl Application for Clock { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Tick(local_time) => { - let now = local_time.into(); + let now = local_time; if now != self.now { self.now = now; @@ -54,140 +56,78 @@ impl Application for Clock { } fn subscription(&self) -> Subscription<Message> { - time::every(std::time::Duration::from_millis(500)).map(Message::Tick) + time::every(std::time::Duration::from_millis(500)) + .map(|_| Message::Tick(chrono::Local::now())) } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + let canvas = Canvas::new(self) .width(Length::Units(400)) - .height(Length::Units(400)) - .push(self.clock.with(&self.now)); + .height(Length::Units(400)); Container::new(canvas) .width(Length::Fill) .height(Length::Fill) + .padding(20) .center_x() .center_y() .into() } } -#[derive(Debug, PartialEq, Eq)] -struct LocalTime { - hour: u32, - minute: u32, - second: u32, -} - -impl From<chrono::DateTime<chrono::Local>> for LocalTime { - fn from(date_time: chrono::DateTime<chrono::Local>) -> LocalTime { +impl canvas::Program<Message> for Clock { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { use chrono::Timelike; - LocalTime { - hour: date_time.hour(), - minute: date_time.minute(), - second: date_time.second(), - } - } -} + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; -impl canvas::Drawable for LocalTime { - fn draw(&self, frame: &mut canvas::Frame) { - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 2.0; - let offset = Vector::new(center.x, center.y); - - let clock = canvas::Path::new(|path| path.circle(center, radius)); - - frame.fill( - &clock, - canvas::Fill::Color(Color::from_rgb8(0x12, 0x93, 0xD8)), - ); - - fn draw_hand( - n: u32, - total: u32, - length: f32, - offset: Vector, - path: &mut canvas::path::Builder, - ) { - let turns = n as f32 / total as f32; - let t = 2.0 * std::f32::consts::PI * (turns - 0.25); - - let x = length * t.cos(); - let y = length * t.sin(); - - path.line_to(Point::new(x, y) + offset); - } + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); - let hour_and_minute_hands = canvas::Path::new(|path| { - path.move_to(center); - draw_hand(self.hour, 12, 0.5 * radius, offset, path); + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); - path.move_to(center); - draw_hand(self.minute, 60, 0.8 * radius, offset, path) - }); + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - frame.stroke( - &hour_and_minute_hands, - canvas::Stroke { - width: 6.0, + let thin_stroke = Stroke { + width: radius / 100.0, color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }, - ); - - let second_hand = canvas::Path::new(|path| { - path.move_to(center); - draw_hand(self.second, 60, 0.8 * radius, offset, path) + line_cap: LineCap::Round, + ..Stroke::default() + }; + + let wide_stroke = Stroke { + width: thin_stroke.width * 3.0, + ..thin_stroke + }; + + frame.translate(Vector::new(center.x, center.y)); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke); + }); + + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke); + }) }); - frame.stroke( - &second_hand, - canvas::Stroke { - width: 3.0, - color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }, - ); + vec![clock] } } -mod time { - use iced::futures; +fn hand_rotation(n: u32, total: u32) -> f32 { + let turns = n as f32 / total as f32; - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<chrono::DateTime<chrono::Local>> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = chrono::DateTime<chrono::Local>; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| chrono::Local::now()) - .boxed() - } - } + 2.0 * std::f32::consts::PI * turns } diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml new file mode 100644 index 00000000..00f33e20 --- /dev/null +++ b/examples/color_palette/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "color_palette" +version = "0.1.0" +authors = ["Clark Moody <clark@clarkmoody.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "palette"] } +palette = "0.5.0" diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md new file mode 100644 index 00000000..e70188f8 --- /dev/null +++ b/examples/color_palette/README.md @@ -0,0 +1,15 @@ +## Color palette + +A color palette generator, based on a user-defined root color. + +<div align="center"> + <a href="https://gfycat.com/dirtylonebighornsheep"> + <img src="https://github.com/hecrj/iced/raw/1a8d253611d3796b0a32b2f096bb54565a5292e0/examples/color_palette/screenshot.png"> + </a> +</div> + +You can run it with `cargo run`: + +``` +cargo run --package color_palette +``` diff --git a/examples/color_palette/screenshot.png b/examples/color_palette/screenshot.png Binary files differnew file mode 100644 index 00000000..aa4772e0 --- /dev/null +++ b/examples/color_palette/screenshot.png diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs new file mode 100644 index 00000000..bb2c61cb --- /dev/null +++ b/examples/color_palette/src/main.rs @@ -0,0 +1,462 @@ +use iced::canvas::{self, Cursor, Frame, Geometry, Path}; +use iced::{ + slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, Length, + Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector, + VerticalAlignment, +}; +use palette::{self, Hsl, Limited, Srgb}; +use std::marker::PhantomData; +use std::ops::RangeInclusive; + +pub fn main() -> iced::Result { + ColorPalette::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +pub struct ColorPalette { + theme: Theme, + rgb: ColorPicker<Color>, + hsl: ColorPicker<palette::Hsl>, + hsv: ColorPicker<palette::Hsv>, + hwb: ColorPicker<palette::Hwb>, + lab: ColorPicker<palette::Lab>, + lch: ColorPicker<palette::Lch>, +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + RgbColorChanged(Color), + HslColorChanged(palette::Hsl), + HsvColorChanged(palette::Hsv), + HwbColorChanged(palette::Hwb), + LabColorChanged(palette::Lab), + LchColorChanged(palette::Lch), +} + +impl Sandbox for ColorPalette { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Color palette - Iced") + } + + fn update(&mut self, message: Message) { + let srgb = match message { + Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), + Message::HslColorChanged(hsl) => palette::Srgb::from(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), + }; + + self.theme = Theme::new(srgb.clamp()); + } + + fn view(&mut 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() + } +} + +#[derive(Debug)] +pub struct Theme { + lower: Vec<Color>, + base: Color, + higher: Vec<Color>, + canvas_cache: canvas::Cache, +} + +impl Theme { + pub fn new(base: impl Into<Color>) -> Theme { + use palette::{Hue, Shade}; + + let base = base.into(); + + // Convert to HSL color for manipulation + let hsl = Hsl::from(Srgb::from(base)); + + let lower = [ + hsl.shift_hue(-135.0).lighten(0.075), + hsl.shift_hue(-120.0), + hsl.shift_hue(-105.0).darken(0.075), + hsl.darken(0.075), + ]; + + let higher = [ + hsl.lighten(0.075), + hsl.shift_hue(105.0).darken(0.075), + hsl.shift_hue(120.0), + hsl.shift_hue(135.0).lighten(0.075), + ]; + + Theme { + lower: lower + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + base, + higher: higher + .iter() + .map(|&color| Srgb::from(color).clamp().into()) + .collect(), + canvas_cache: canvas::Cache::default(), + } + } + + pub fn len(&self) -> usize { + self.lower.len() + self.higher.len() + 1 + } + + pub fn colors(&self) -> impl Iterator<Item = &Color> { + self.lower + .iter() + .chain(std::iter::once(&self.base)) + .chain(self.higher.iter()) + } + + pub fn view(&mut self) -> Element<Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn draw(&self, frame: &mut Frame) { + let pad = 20.0; + + let box_size = Size { + width: frame.width() / self.len() as f32, + height: frame.height() / 2.0 - pad, + }; + + let triangle = Path::new(|path| { + path.move_to(Point { x: 0.0, y: -0.5 }); + path.line_to(Point { x: -0.5, y: 0.0 }); + path.line_to(Point { x: 0.5, y: 0.0 }); + path.close(); + }); + + let mut text = canvas::Text { + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Top, + size: 15.0, + ..canvas::Text::default() + }; + + for (i, &color) in self.colors().enumerate() { + let anchor = Point { + x: (i as f32) * box_size.width, + y: 0.0, + }; + frame.fill_rectangle(anchor, box_size, color); + + // We show a little indicator for the base color + if color == self.base { + let triangle_x = anchor.x + box_size.width / 2.0; + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, 0.0)); + frame.scale(10.0); + frame.rotate(std::f32::consts::PI); + + frame.fill(&triangle, Color::WHITE); + }); + + frame.with_save(|frame| { + frame.translate(Vector::new(triangle_x, box_size.height)); + frame.scale(10.0); + + frame.fill(&triangle, Color::WHITE); + }); + } + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height, + }, + ..text + }); + } + + text.vertical_alignment = VerticalAlignment::Bottom; + + let hsl = Hsl::from(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 anchor = Point { + x: (i as f32) * box_size.width, + y: box_size.height + 2.0 * pad, + }; + + frame.fill_rectangle(anchor, box_size, color); + + frame.fill_text(canvas::Text { + content: color_hex_string(&color), + position: Point { + x: anchor.x + box_size.width / 2.0, + y: box_size.height + 2.0 * pad, + }, + ..text + }); + } + } +} + +impl canvas::Program<Message> for Theme { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { + let theme = self.canvas_cache.draw(bounds.size(), |frame| { + self.draw(frame); + }); + + vec![theme] + } +} + +impl Default for Theme { + fn default() -> Self { + Theme::new(Color::from_rgb8(75, 128, 190)) + } +} + +fn color_hex_string(color: &Color) -> String { + format!( + "#{:x}{:x}{:x}", + (255.0 * color.r).round() as u8, + (255.0 * color.g).round() as u8, + (255.0 * color.b).round() as u8 + ) +} + +#[derive(Default)] +struct ColorPicker<C: ColorSpace> { + sliders: [slider::State; 3], + color_space: PhantomData<C>, +} + +trait ColorSpace: Sized { + const LABEL: &'static str; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3]; + + fn new(a: f32, b: f32, c: f32) -> Self; + + fn components(&self) -> [f32; 3]; + + fn to_string(&self) -> String; +} + +impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { + fn view(&mut 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, + 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) + } + + Row::new() + .spacing(10) + .align_items(Align::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() + } +} + +impl ColorSpace for Color { + const LABEL: &'static str = "RGB"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(r: f32, g: f32, b: f32) -> Self { + Color::from_rgb(r, g, b) + } + + fn components(&self) -> [f32; 3] { + [self.r, self.g, self.b] + } + + fn to_string(&self) -> String { + format!( + "rgb({:.0}, {:.0}, {:.0})", + 255.0 * self.r, + 255.0 * self.g, + 255.0 * self.b + ) + } +} + +impl ColorSpace for palette::Hsl { + const LABEL: &'static str = "HSL"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, lightness: f32) -> Self { + palette::Hsl::new( + palette::RgbHue::from_degrees(hue), + saturation, + lightness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.saturation, + self.lightness, + ] + } + + fn to_string(&self) -> String { + format!( + "hsl({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.lightness + ) + } +} + +impl ColorSpace for palette::Hsv { + const LABEL: &'static str = "HSV"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, saturation: f32, value: f32) -> Self { + palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value) + } + + fn components(&self) -> [f32; 3] { + [self.hue.to_positive_degrees(), self.saturation, self.value] + } + + fn to_string(&self) -> String { + format!( + "hsv({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.saturation, + 100.0 * self.value + ) + } +} + +impl ColorSpace for palette::Hwb { + const LABEL: &'static str = "HWB"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; + + fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { + palette::Hwb::new( + palette::RgbHue::from_degrees(hue), + whiteness, + blackness, + ) + } + + fn components(&self) -> [f32; 3] { + [ + self.hue.to_positive_degrees(), + self.whiteness, + self.blackness, + ] + } + + fn to_string(&self) -> String { + format!( + "hwb({:.1}, {:.1}%, {:.1}%)", + self.hue.to_positive_degrees(), + 100.0 * self.whiteness, + 100.0 * self.blackness + ) + } +} + +impl ColorSpace for palette::Lab { + const LABEL: &'static str = "Lab"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; + + fn new(l: f32, a: f32, b: f32) -> Self { + palette::Lab::new(l, a, b) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.a, self.b] + } + + fn to_string(&self) -> String { + format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b) + } +} + +impl ColorSpace for palette::Lch { + const LABEL: &'static str = "Lch"; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = + [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; + + fn new(l: f32, chroma: f32, hue: f32) -> Self { + palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue)) + } + + fn components(&self) -> [f32; 3] { + [self.l, self.chroma, self.hue.to_positive_degrees()] + } + + fn to_string(&self) -> String { + format!( + "Lch({:.1}, {:.1}, {:.1})", + self.l, + self.chroma, + self.hue.to_positive_degrees() + ) + } +} diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index bde0ea94..e0b2ebd6 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,6 +1,6 @@ use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text}; -pub fn main() { +pub fn main() -> iced::Result { Counter::run(Settings::default()) } diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml index 30747dc0..3942538d 100644 --- a/examples/custom_widget/Cargo.toml +++ b/examples/custom_widget/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0a570745..36f468c7 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -9,23 +9,26 @@ mod circle { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::{Backend, Defaults, Primitive, Renderer}; use iced_native::{ - layout, Background, Color, Element, Hasher, Layout, Length, - MouseCursor, Point, Size, Widget, + layout, mouse, Background, Color, Element, Hasher, Layout, Length, + Point, Rectangle, Size, Widget, }; - use iced_wgpu::{Defaults, Primitive, Renderer}; pub struct Circle { - radius: u16, + radius: f32, } impl Circle { - pub fn new(radius: u16) -> Self { + pub fn new(radius: f32) -> Self { Self { radius } } } - impl<Message> Widget<Message, Renderer> for Circle { + impl<Message, B> Widget<Message, Renderer<B>> for Circle + where + B: Backend, + { fn width(&self) -> Length { Length::Shrink } @@ -36,43 +39,44 @@ mod circle { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, _limits: &layout::Limits, ) -> layout::Node { - layout::Node::new(Size::new( - f32::from(self.radius) * 2.0, - f32::from(self.radius) * 2.0, - )) + layout::Node::new(Size::new(self.radius * 2.0, self.radius * 2.0)) } fn hash_layout(&self, state: &mut Hasher) { use std::hash::Hash; - self.radius.hash(state); + self.radius.to_bits().hash(state); } fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, _cursor_position: Point, - ) -> (Primitive, MouseCursor) { + _viewport: &Rectangle, + ) -> (Primitive, mouse::Interaction) { ( Primitive::Quad { bounds: layout.bounds(), background: Background::Color(Color::BLACK), border_radius: self.radius, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Circle + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } @@ -84,12 +88,12 @@ use iced::{ Slider, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } struct Example { - radius: u16, + radius: f32, slider: slider::State, } @@ -103,7 +107,7 @@ impl Sandbox for Example { fn new() -> Self { Example { - radius: 50, + radius: 50.0, slider: slider::State::new(), } } @@ -115,7 +119,7 @@ impl Sandbox for Example { fn update(&mut self, message: Message) { match message { Message::RadiusChanged(radius) => { - self.radius = radius.round() as u16; + self.radius = radius; } } } @@ -127,13 +131,16 @@ impl Sandbox for Example { .max_width(500) .align_items(Align::Center) .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {}", self.radius.to_string()))) - .push(Slider::new( - &mut self.slider, - 1.0..=100.0, - f32::from(self.radius), - Message::RadiusChanged, - )); + .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) .width(Length::Fill) diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml new file mode 100644 index 00000000..4b05e7dc --- /dev/null +++ b/examples/download_progress/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "download_progress" +version = "0.1.0" +authors = ["Songtronix <contact@songtronix.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["tokio_old"] } +iced_native = { path = "../../native" } +iced_futures = { path = "../../futures" } +reqwest = "0.10" diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md new file mode 100644 index 00000000..c606c5f9 --- /dev/null +++ b/examples/download_progress/README.md @@ -0,0 +1,17 @@ +## Download progress + +A basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. + +The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress. + +<div align="center"> + <a href="https://gfycat.com/wildearlyafricanwilddog"> + <img src="https://thumbs.gfycat.com/WildEarlyAfricanwilddog-small.gif"> + </a> +</div> + +You can run it with `cargo run`: + +``` +cargo run --package download_progress +``` diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs new file mode 100644 index 00000000..f46a01f7 --- /dev/null +++ b/examples/download_progress/src/download.rs @@ -0,0 +1,112 @@ +use iced_futures::futures; + +// Just a little utility function +pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> { + iced::Subscription::from_recipe(Download { + url: url.to_string(), + }) +} + +pub struct Download { + url: String, +} + +// Make sure iced can use our download stream +impl<H, I> iced_native::subscription::Recipe<H, I> for Download +where + H: std::hash::Hasher, +{ + type Output = Progress; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::<Self>().hash(state); + self.url.hash(state); + } + + fn stream( + self: Box<Self>, + _input: futures::stream::BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + Box::pin(futures::stream::unfold( + State::Ready(self.url), + |state| async move { + match state { + State::Ready(url) => { + let response = reqwest::get(&url).await; + + match response { + Ok(response) => { + if let Some(total) = response.content_length() { + Some(( + Progress::Started, + State::Downloading { + response, + total, + downloaded: 0, + }, + )) + } else { + Some((Progress::Errored, State::Finished)) + } + } + Err(_) => { + Some((Progress::Errored, State::Finished)) + } + } + } + State::Downloading { + mut response, + total, + downloaded, + } => match response.chunk().await { + Ok(Some(chunk)) => { + let downloaded = downloaded + chunk.len() as u64; + + let percentage = + (downloaded as f32 / total as f32) * 100.0; + + Some(( + Progress::Advanced(percentage), + State::Downloading { + response, + total, + downloaded, + }, + )) + } + Ok(None) => Some((Progress::Finished, State::Finished)), + Err(_) => Some((Progress::Errored, State::Finished)), + }, + State::Finished => { + // 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; + + None + } + } + }, + )) + } +} + +#[derive(Debug, Clone)] +pub enum Progress { + Started, + Advanced(f32), + Finished, + Errored, +} + +pub enum State { + Ready(String), + Downloading { + response: reqwest::Response, + total: u64, + downloaded: u64, + }, + Finished, +} diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs new file mode 100644 index 00000000..77b01354 --- /dev/null +++ b/examples/download_progress/src/main.rs @@ -0,0 +1,144 @@ +use iced::{ + button, executor, Align, Application, Button, Column, Command, Container, + Element, Length, ProgressBar, Settings, Subscription, Text, +}; + +mod download; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Debug)] +enum Example { + Idle { button: button::State }, + Downloading { progress: f32 }, + Finished { button: button::State }, + Errored { button: button::State }, +} + +#[derive(Debug, Clone)] +pub enum Message { + Download, + DownloadProgressed(download::Progress), +} + +impl Application for Example { + type Executor = executor::Default; + type Message = Message; + type Flags = (); + + fn new(_flags: ()) -> (Example, Command<Message>) { + ( + Example::Idle { + button: button::State::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Download progress - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Download => match self { + Example::Idle { .. } + | Example::Finished { .. } + | Example::Errored { .. } => { + *self = Example::Downloading { progress: 0.0 }; + } + _ => {} + }, + Message::DownloadProgressed(message) => match self { + Example::Downloading { progress } => match message { + download::Progress::Started => { + *progress = 0.0; + } + download::Progress::Advanced(percentage) => { + *progress = percentage; + } + download::Progress::Finished => { + *self = Example::Finished { + button: button::State::new(), + } + } + download::Progress::Errored => { + *self = Example::Errored { + button: button::State::new(), + }; + } + }, + _ => {} + }, + }; + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + match self { + Example::Downloading { .. } => { + download::file("https://speed.hetzner.de/100MB.bin") + .map(Message::DownloadProgressed) + } + _ => Subscription::none(), + } + } + + fn view(&mut self) -> Element<Message> { + let current_progress = match self { + Example::Idle { .. } => 0.0, + Example::Downloading { progress } => *progress, + Example::Finished { .. } => 100.0, + Example::Errored { .. } => 0.0, + }; + + let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); + + let control: Element<_> = match self { + Example::Idle { button } => { + Button::new(button, Text::new("Start the download!")) + .on_press(Message::Download) + .into() + } + Example::Finished { button } => Column::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new("Download finished!")) + .push( + Button::new(button, Text::new("Start again")) + .on_press(Message::Download), + ) + .into(), + Example::Downloading { .. } => { + Text::new(format!("Downloading... {:.2}%", current_progress)) + .into() + } + Example::Errored { button } => Column::new() + .spacing(10) + .align_items(Align::Center) + .push(Text::new("Something went wrong :(")) + .push( + Button::new(button, Text::new("Try again")) + .on_press(Message::Download), + ) + .into(), + }; + + let content = Column::new() + .spacing(10) + .padding(10) + .align_items(Align::Center) + .push(progress_bar) + .push(control); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 0c9dca05..6eba6aad 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -3,7 +3,7 @@ use iced::{ Element, Length, Settings, Subscription, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Events::run(Settings::default()) } @@ -22,8 +22,9 @@ enum Message { impl Application for Events { type Executor = executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Events, Command<Message>) { + fn new(_flags: ()) -> (Events, Command<Message>) { (Events::default(), Command::none()) } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..9c4172c4 --- /dev/null +++ b/examples/game_of_life/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +tokio = { version = "0.3", features = ["sync"] } +itertools = "0.9" +rustc-hash = "1.1" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..aa39201c --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,22 @@ +## Game of Life + +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The __[`main`]__ file contains the relevant code of the example. + +<div align="center"> + <a href="https://gfycat.com/WhichPaltryChick"> + <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[`main`]: src/main.rs +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..e18bd6e0 --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,885 @@ +//! This example showcases an interactive version of the Game of Life, invented +//! by John Conway. It leverages a `Canvas` together with other widgets. +mod preset; +mod style; + +use grid::Grid; +use iced::button::{self, Button}; +use iced::executor; +use iced::pick_list::{self, PickList}; +use iced::slider::{self, Slider}; +use iced::time; +use iced::{ + Align, Application, Checkbox, Column, Command, Container, Element, Length, + Row, Settings, Subscription, Text, +}; +use preset::Preset; +use std::time::{Duration, Instant}; + +pub fn main() -> iced::Result { + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + controls: Controls, + is_playing: bool, + queued_ticks: usize, + speed: usize, + next_speed: Option<usize>, + version: usize, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message, usize), + Tick(Instant), + TogglePlayback, + ToggleGrid(bool), + Next, + Clear, + SpeedChanged(f32), + PresetPicked(Preset), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self { + speed: 5, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Grid(message, version) => { + if version == self.version { + self.grid.update(message); + } + } + Message::Tick(_) | Message::Next => { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + + if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + self.queued_ticks = 0; + + let version = self.version; + + return Command::perform(task, move |message| { + Message::Grid(message, version) + }); + } + } + Message::TogglePlayback => { + self.is_playing = !self.is_playing; + } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } + Message::Clear => { + self.grid.clear(); + self.version += 1; + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; + } + } + Message::PresetPicked(new_preset) => { + self.grid = Grid::from_preset(new_preset); + self.version += 1; + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element<Message> { + let version = self.version; + let selected_speed = self.next_speed.unwrap_or(self.speed); + let controls = self.controls.view( + 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); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +mod grid { + use crate::Preset; + use iced::{ + canvas::event::{self, Event}, + canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text}, + mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, + Size, Vector, VerticalAlignment, + }; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; + use std::ops::RangeInclusive; + use std::time::{Duration, Instant}; + + pub struct Grid { + state: State, + preset: Preset, + interaction: Interaction, + life_cache: Cache, + grid_cache: Cache, + translation: Vector, + scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, + } + + #[derive(Debug, Clone)] + pub enum Message { + Populate(Cell), + Unpopulate(Cell), + Ticked { + result: Result<Life, TickError>, + tick_duration: Duration, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, + } + + impl Default for Grid { + fn default() -> Self { + Self::from_preset(Preset::default()) + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn from_preset(preset: Preset) -> Self { + Self { + state: State::with_life( + preset + .life() + .into_iter() + .map(|(i, j)| Cell { i, j }) + .collect(), + ), + preset, + interaction: Interaction::None, + life_cache: Cache::default(), + grid_cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, + } + } + + pub fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Message>> { + let tick = self.state.tick(amount)?; + + self.last_queued_ticks = amount; + + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + tick_duration, + } + }) + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate(cell) => { + self.state.populate(cell); + self.life_cache.clear(); + + self.preset = Preset::Custom; + } + Message::Unpopulate(cell) => { + self.state.unpopulate(&cell); + self.life_cache.clear(); + + self.preset = Preset::Custom; + } + Message::Ticked { + result: Ok(life), + tick_duration, + } => { + self.state.update(life); + self.life_cache.clear(); + + self.last_tick_duration = tick_duration; + } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.preset = Preset::Custom; + + self.life_cache.clear(); + } + + pub fn preset(&self) -> Preset { + self.preset + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Region { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } + } + + impl<'a> canvas::Program<Message> for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option<Message>) { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + self.interaction = Interaction::None; + } + + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; + + let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); + + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) + } else { + (Some(Message::Populate(cell)), None) + }; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(button) => { + let message = match button { + mouse::Button::Left => { + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + self.interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; + + None + } + _ => None, + }; + + (event::Status::Captured, message) + } + mouse::Event::CursorMoved { .. } => { + let message = match self.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 + } + _ => None, + }; + + let event_status = match self.interaction { + Interaction::None => event::Status::Ignored, + _ => event::Status::Captured, + }; + + (event_status, message) + } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING + { + let old_scaling = self.scaling; + + self.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(); + } + + (event::Status::Captured, None) + } + }, + _ => (event::Status::Ignored, None), + }, + _ => (event::Status::Ignored, None), + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); + + let life = self.life_cache.draw(bounds.size(), |frame| { + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + + for cell in region.cull(self.state.cells()) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.size()); + + let hovered_cell = + cursor.position_in(&bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); + + if let Some(cell) = hovered_cell { + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); + } + + let cell_count = self.state.cell_count(); + + frame.fill_text(Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + + frame.into_geometry() + }; + + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, overlay] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + match self.interaction { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), + } + } + } + + #[derive(Default)] + struct State { + life: Life, + births: FxHashSet<Cell>, + is_ticking: bool, + } + + impl State { + pub fn with_life(life: Life) -> Self { + Self { + life, + ..Self::default() + } + } + + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator<Item = &Cell> { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Result<Life, TickError>>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } + } + + #[derive(Clone, Default)] + pub struct Life { + cells: FxHashSet<Cell>, + } + + impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator<Item = &Cell> { + self.cells.iter() + } + } + + impl std::iter::FromIterator<Cell> for Life { + fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self { + Life { + cells: iter.into_iter().collect(), + } + } + } + + impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator<Item = Cell> { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + } + + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn rows(&self) -> RangeInclusive<isize> { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive<isize> { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator<Item = &'a Cell>, + ) -> impl Iterator<Item = &'a Cell> { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + + 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(Align::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(Align::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() + } +} diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs new file mode 100644 index 00000000..05157b6a --- /dev/null +++ b/examples/game_of_life/src/preset.rs @@ -0,0 +1,142 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Preset { + Custom, + XKCD, + Glider, + SmallExploder, + Exploder, + TenCellRow, + LightweightSpaceship, + Tumbler, + GliderGun, + Acorn, +} + +pub static ALL: &[Preset] = &[ + Preset::Custom, + Preset::XKCD, + Preset::Glider, + Preset::SmallExploder, + Preset::Exploder, + Preset::TenCellRow, + Preset::LightweightSpaceship, + Preset::Tumbler, + Preset::GliderGun, + Preset::Acorn, +]; + +impl Preset { + pub fn life(self) -> Vec<(isize, isize)> { + #[rustfmt::skip] + let cells = match self { + Preset::Custom => vec![], + Preset::XKCD => vec![ + " xxx ", + " x x ", + " x x ", + " x ", + "x xxx ", + " x x x ", + " x x", + " x x ", + " x x ", + ], + Preset::Glider => vec![ + " x ", + " x", + "xxx" + ], + Preset::SmallExploder => vec![ + " x ", + "xxx", + "x x", + " x ", + ], + Preset::Exploder => vec![ + "x x x", + "x x", + "x x", + "x x", + "x x x", + ], + Preset::TenCellRow => vec![ + "xxxxxxxxxx", + ], + Preset::LightweightSpaceship => vec![ + " xxxxx", + "x x", + " x", + "x x ", + ], + Preset::Tumbler => vec![ + " xx xx ", + " xx xx ", + " x x ", + "x x x x", + "x x x x", + "xx xx", + ], + Preset::GliderGun => vec![ + " x ", + " x x ", + " xx xx xx", + " x x xx xx", + "xx x x xx ", + "xx x x xx x x ", + " x x x ", + " x x ", + " xx ", + ], + Preset::Acorn => vec![ + " x ", + " x ", + "xx xxx", + ], + }; + + let start_row = -(cells.len() as isize / 2); + + cells + .into_iter() + .enumerate() + .flat_map(|(i, cells)| { + let start_column = -(cells.len() as isize / 2); + + cells + .chars() + .enumerate() + .filter(|(_, c)| !c.is_whitespace()) + .map(move |(j, _)| { + (start_row + i as isize, start_column + j as isize) + }) + }) + .collect() + } +} + +impl Default for Preset { + fn default() -> Preset { + Preset::XKCD + } +} + +impl std::fmt::Display for Preset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Preset::Custom => "Custom", + Preset::XKCD => "xkcd #2293", + Preset::Glider => "Glider", + Preset::SmallExploder => "Small Exploder", + Preset::Exploder => "Exploder", + Preset::TenCellRow => "10 Cell Row", + Preset::LightweightSpaceship => "Lightweight spaceship", + Preset::Tumbler => "Tumbler", + Preset::GliderGun => "Gosper Glider Gun", + Preset::Acorn => "Acorn", + } + ) + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..6605826f --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,188 @@ +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, + } + } + + 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/Cargo.toml b/examples/geometry/Cargo.toml index 9df52454..34eec4fb 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 13a687ab..f650b2c1 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -10,13 +10,13 @@ mod rainbow { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, - Widget, - }; - use iced_wgpu::{ + use iced_graphics::{ triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, + Backend, Defaults, Primitive, Renderer, + }; + use iced_native::{ + layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size, + Vector, Widget, }; pub struct Rainbow; @@ -27,7 +27,10 @@ mod rainbow { } } - impl<Message> Widget<Message, Renderer> for Rainbow { + impl<Message, B> Widget<Message, Renderer<B>> for Rainbow + where + B: Backend, + { fn width(&self) -> Length { Length::Fill } @@ -38,7 +41,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -50,11 +53,12 @@ mod rainbow { fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, cursor_position: Point, - ) -> (Primitive, MouseCursor) { + _viewport: &Rectangle, + ) -> (Primitive, mouse::Interaction) { let b = layout.bounds(); // R O Y G B I V @@ -85,66 +89,72 @@ mod rainbow { let posn_l = [0.0, b.height / 2.0]; ( - Primitive::Mesh2D { - origin: Point::new(b.x, b.y), - buffers: Mesh2D { - vertices: vec![ - Vertex2D { - position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], - }, - Vertex2D { - position: posn_tl, - color: color_r, - }, - Vertex2D { - position: posn_t, - color: color_o, - }, - Vertex2D { - position: posn_tr, - color: color_y, - }, - Vertex2D { - position: posn_r, - color: color_g, - }, - Vertex2D { - position: posn_br, - color: color_gb, - }, - Vertex2D { - position: posn_b, - color: color_b, - }, - Vertex2D { - position: posn_bl, - color: color_i, - }, - Vertex2D { - position: posn_l, - color: color_v, - }, - ], - indices: vec![ - 0, 1, 2, // TL - 0, 2, 3, // T - 0, 3, 4, // TR - 0, 4, 5, // R - 0, 5, 6, // BR - 0, 6, 7, // B - 0, 7, 8, // BL - 0, 8, 1, // L - ], - }, + Primitive::Translate { + translation: Vector::new(b.x, b.y), + content: Box::new(Primitive::Mesh2D { + size: b.size(), + buffers: Mesh2D { + vertices: vec![ + Vertex2D { + position: posn_center, + color: [1.0, 1.0, 1.0, 1.0], + }, + Vertex2D { + position: posn_tl, + color: color_r, + }, + Vertex2D { + position: posn_t, + color: color_o, + }, + Vertex2D { + position: posn_tr, + color: color_y, + }, + Vertex2D { + position: posn_r, + color: color_g, + }, + Vertex2D { + position: posn_br, + color: color_gb, + }, + Vertex2D { + position: posn_b, + color: color_b, + }, + Vertex2D { + position: posn_bl, + color: color_i, + }, + Vertex2D { + position: posn_l, + color: color_v, + }, + ], + indices: vec![ + 0, 1, 2, // TL + 0, 2, 3, // T + 0, 3, 4, // TR + 0, 4, 5, // R + 0, 5, 6, // BR + 0, 6, 7, // B + 0, 7, 8, // BL + 0, 8, 1, // L + ], + }, + }), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } @@ -156,7 +166,7 @@ use iced::{ }; use rainbow::Rainbow; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index afc2c791..4515502f 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu" } -env_logger = "0.7" +env_logger = "0.8" diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 0457a058..824f9f53 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,15 +1,15 @@ -use crate::Scene; - use iced_wgpu::Renderer; use iced_winit::{ - slider, Align, Color, Column, Element, Length, Row, Slider, Text, + slider, Align, Color, Column, Command, Element, Length, Program, Row, + Slider, Text, }; pub struct Controls { + background_color: Color, sliders: [slider::State; 3], } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), } @@ -17,61 +17,64 @@ pub enum Message { impl Controls { pub fn new() -> Controls { Controls { + background_color: Color::BLACK, sliders: Default::default(), } } - pub fn update(&self, message: Message, scene: &mut Scene) { + pub fn background_color(&self) -> Color { + self.background_color + } +} + +impl Program for Controls { + type Renderer = Renderer; + type Message = Message; + + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::BackgroundColorChanged(color) => { - scene.background_color = color; + self.background_color = color; } } + + Command::none() } - pub fn view<'a>( - &'a mut self, - scene: &Scene, - ) -> Element<'a, Message, Renderer> { + fn view(&mut self) -> Element<Message, Renderer> { let [r, g, b] = &mut self.sliders; - let background_color = scene.background_color; + let background_color = self.background_color; let sliders = Row::new() .width(Length::Units(500)) .spacing(20) - .push(Slider::new( - r, - 0.0..=1.0, - scene.background_color.r, - move |r| { + .push( + Slider::new(r, 0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color }) - }, - )) - .push(Slider::new( - g, - 0.0..=1.0, - scene.background_color.g, - move |g| { + }) + .step(0.01), + ) + .push( + Slider::new(g, 0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color }) - }, - )) - .push(Slider::new( - b, - 0.0..=1.0, - scene.background_color.b, - move |b| { + }) + .step(0.01), + ) + .push( + Slider::new(b, 0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color }) - }, - )); + }) + .step(0.01), + ); Row::new() .width(Length::Fill) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 2cb89ffc..9b52f3a5 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,12 +4,12 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::{ - wgpu, window::SwapChain, Primitive, Renderer, Settings, Target, -}; -use iced_winit::{winit, Cache, Clipboard, MouseCursor, Size, UserInterface}; +use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; +use iced_winit::{conversion, futures, program, winit, Debug, Size}; +use futures::task::SpawnExt; use winit::{ + dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; @@ -20,45 +20,79 @@ pub fn main() { // Initialize winit let event_loop = EventLoop::new(); let window = winit::window::Window::new(&event_loop).unwrap(); - let mut logical_size = - window.inner_size().to_logical(window.scale_factor()); - let mut modifiers = ModifiersState::default(); - // Initialize WGPU - let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::Default, - backends: wgpu::BackendBit::PRIMARY, - }) - .expect("Request adapter"); + let physical_size = window.inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); + let mut modifiers = ModifiersState::default(); - let (mut device, mut queue) = - adapter.request_device(&wgpu::DeviceDescriptor { - extensions: wgpu::Extensions { - anisotropic_filtering: false, - }, - limits: wgpu::Limits::default(), - }); + // Initialize wgpu + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + let surface = unsafe { instance.create_surface(&window) }; + + let (mut device, queue) = futures::executor::block_on(async { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::Default, + compatible_surface: Some(&surface), + }) + .await + .expect("Request adapter"); + + adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: false, + }, + None, + ) + .await + .expect("Request device") + }); - let surface = wgpu::Surface::create(&window); let format = wgpu::TextureFormat::Bgra8UnormSrgb; let mut swap_chain = { let size = window.inner_size(); - SwapChain::new(&device, &surface, format, size.width, size.height) + device.create_swap_chain( + &surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, + ) }; let mut resized = false; - // Initialize iced - let mut events = Vec::new(); - let mut cache = Some(Cache::default()); - let mut renderer = Renderer::new(&mut device, Settings::default()); - let mut output = (Primitive::None, MouseCursor::OutOfBounds); - let clipboard = Clipboard::new(&window); + // Initialize staging belt and local pool + let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); + let mut local_pool = futures::executor::LocalPool::new(); // Initialize scene and GUI controls - let mut scene = Scene::new(&device); - let mut controls = Controls::new(); + let scene = Scene::new(&mut device); + let controls = Controls::new(); + + // Initialize iced + let mut debug = Debug::new(); + let mut renderer = + Renderer::new(Backend::new(&mut device, Settings::default())); + + let mut state = program::State::new( + controls, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); // Run event loop event_loop.run(move |event, _, control_flow| { @@ -68,135 +102,121 @@ pub fn main() { match event { Event::WindowEvent { event, .. } => { match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } WindowEvent::ModifiersChanged(new_modifiers) => { modifiers = new_modifiers; } WindowEvent::Resized(new_size) => { - logical_size = - new_size.to_logical(window.scale_factor()); + viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor(), + ); + resized = true; } WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } - _ => {} } // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( - event, + &event, window.scale_factor(), modifiers, ) { - events.push(event); + state.queue_event(event); } } Event::MainEventsCleared => { - // If no relevant events happened, we can simply skip this - if events.is_empty() { - return; - } - - // We need to: - // 1. Process events of our user interface. - // 2. Update state as a result of any interaction. - // 3. Generate a new output for our renderer. - - // First, we build our user interface. - let mut user_interface = UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), - &mut renderer, - ); - - // Then, we process the events, obtaining messages in return. - let messages = user_interface.update( - events.drain(..), - clipboard.as_ref().map(|c| c as _), - &renderer, - ); - - let user_interface = if messages.is_empty() { - // If there are no messages, no interactions we care about have - // happened. We can simply leave our user interface as it is. - user_interface - } else { - // If there are messages, we need to update our state - // accordingly and rebuild our user interface. - // We can only do this if we drop our user interface first - // by turning it into its cache. - cache = Some(user_interface.into_cache()); - - // In this example, `Controls` is the only part that cares - // about messages, so updating our state is pretty - // straightforward. - for message in messages { - controls.update(message, &mut scene); - } - - // Once the state has been changed, we rebuild our updated - // user interface. - UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + None, &mut renderer, - ) - }; - - // Finally, we just need to draw a new output for our renderer, - output = user_interface.draw(&mut renderer); - - // update our cache, - cache = Some(user_interface.into_cache()); + &mut debug, + ); - // and request a redraw - window.request_redraw(); + // and request a redraw + window.request_redraw(); + } } Event::RedrawRequested(_) => { if resized { let size = window.inner_size(); - swap_chain = SwapChain::new( - &device, + swap_chain = device.create_swap_chain( &surface, - format, - size.width, - size.height, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, ); + + resized = false; } - let (frame, viewport) = swap_chain.next_frame(); + let frame = swap_chain.get_current_frame().expect("Next frame"); let mut encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { todo: 0 }, + &wgpu::CommandEncoderDescriptor { label: None }, ); - // We draw the scene first - scene.draw(&mut encoder, &frame.view); + let program = state.program(); + + { + // We clear the frame + let mut render_pass = scene.clear( + &frame.output.view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } // And then iced on top - let mouse_cursor = renderer.draw( + let mouse_interaction = renderer.backend_mut().draw( &mut device, + &mut staging_belt, &mut encoder, - Target { - texture: &frame.view, - viewport, - }, - &output, - window.scale_factor(), - &["Some debug information!"], + &frame.output.view, + &viewport, + state.primitive(), + &debug.overlay(), ); // Then we submit the work - queue.submit(&[encoder.finish()]); + staging_belt.finish(); + queue.submit(Some(encoder.finish())); + + // Update the mouse cursor + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); + + // And recall staging buffers + local_pool + .spawner() + .spawn(staging_belt.recall()) + .expect("Recall staging buffers"); - // And update the mouse cursor - window.set_cursor_icon(iced_winit::conversion::mouse_cursor( - mouse_cursor, - )); + local_pool.run_until_stalled(); } _ => {} } diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index efb1921b..03a338c6 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -2,89 +2,68 @@ use iced_wgpu::wgpu; use iced_winit::Color; pub struct Scene { - pub background_color: Color, pipeline: wgpu::RenderPipeline, - bind_group: wgpu::BindGroup, } impl Scene { pub fn new(device: &wgpu::Device) -> Scene { - let (pipeline, bind_group) = build_pipeline(device); + let pipeline = build_pipeline(device); - Scene { - background_color: Color::BLACK, - pipeline, - bind_group, - } + Scene { pipeline } } - pub fn draw( + pub fn clear<'a>( &self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - ) { - let mut rpass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: { - let [r, g, b, a] = - self.background_color.into_linear(); + target: &'a wgpu::TextureView, + encoder: &'a mut wgpu::CommandEncoder, + background_color: Color, + ) -> wgpu::RenderPass<'a> { + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear({ + let [r, g, b, a] = background_color.into_linear(); - wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, - } - }, - }, - ], - depth_stencil_attachment: None, - }); + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } + }), + store: true, + }, + }], + depth_stencil_attachment: None, + }) + } - rpass.set_pipeline(&self.pipeline); - rpass.set_bind_group(0, &self.bind_group, &[]); - rpass.draw(0..3, 0..1); + pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.draw(0..3, 0..1); } } -fn build_pipeline( - device: &wgpu::Device, -) -> (wgpu::RenderPipeline, wgpu::BindGroup) { - let vs = include_bytes!("shader/vert.spv"); - let fs = include_bytes!("shader/frag.spv"); +fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { + let vs_module = + device.create_shader_module(wgpu::include_spirv!("shader/vert.spv")); - let vs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&vs[..])).unwrap(), - ); - - let fs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&fs[..])).unwrap(), - ); - - let bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - bindings: &[], - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &bind_group_layout, - bindings: &[], - }); + let fs_module = + device.create_shader_module(wgpu::include_spirv!("shader/frag.spv")); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&bind_group_layout], + label: None, + push_constant_ranges: &[], + bind_group_layouts: &[], }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &pipeline_layout, + label: None, + layout: Some(&pipeline_layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -96,9 +75,7 @@ fn build_pipeline( rasterization_state: Some(wgpu::RasterizationStateDescriptor { front_face: wgpu::FrontFace::Ccw, cull_mode: wgpu::CullMode::None, - depth_bias: 0, - depth_bias_slope_scale: 0.0, - depth_bias_clamp: 0.0, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -108,12 +85,14 @@ fn build_pipeline( write_mask: wgpu::ColorWrite::ALL, }], depth_stencil_state: None, - index_format: wgpu::IndexFormat::Uint16, - vertex_buffers: &[], + vertex_state: wgpu::VertexStateDescriptor { + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + }, sample_count: 1, sample_mask: !0, alpha_to_coverage_enabled: false, }); - (pipeline, bind_group) + pipeline } diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml new file mode 100644 index 00000000..e489f210 --- /dev/null +++ b/examples/pane_grid/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pane_grid" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } +iced_native = { path = "../../native" } diff --git a/examples/pane_grid/README.md b/examples/pane_grid/README.md new file mode 100644 index 00000000..a4cfcb7d --- /dev/null +++ b/examples/pane_grid/README.md @@ -0,0 +1,28 @@ +## Pane grid + +A grid of panes that can be split, resized, and reorganized. + +This example showcases the `PaneGrid` widget, which features: + +* Vertical and horizontal splits +* Tracking of the last active pane +* Mouse-based resizing +* Drag and drop to reorganize panes +* Hotkey support +* Configurable modifier keys +* API to perform actions programmatically (`split`, `swap`, `resize`, etc.) + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/frailfreshairedaleterrier"> + <img src="https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package pane_grid +``` + +[`main`]: src/main.rs diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs new file mode 100644 index 00000000..3c3256cf --- /dev/null +++ b/examples/pane_grid/src/main.rs @@ -0,0 +1,372 @@ +use iced::{ + button, executor, keyboard, pane_grid, scrollable, Align, Application, + Button, Column, Command, Container, Element, HorizontalAlignment, Length, + PaneGrid, Scrollable, Settings, Subscription, Text, +}; +use iced_native::{event, subscription, Event}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +struct Example { + panes: pane_grid::State<Content>, + panes_created: usize, + focus: Option<pane_grid::Pane>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Split(pane_grid::Axis, pane_grid::Pane), + SplitFocused(pane_grid::Axis), + FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), + Close(pane_grid::Pane), + CloseFocused, +} + +impl Application for Example { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + let (panes, _) = pane_grid::State::new(Content::new(0)); + + ( + Example { + panes, + panes_created: 1, + focus: None, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Pane grid - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Split(axis, pane) => { + let result = self.panes.split( + axis, + &pane, + Content::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + Message::SplitFocused(axis) => { + if let Some(pane) = self.focus { + let result = self.panes.split( + axis, + &pane, + Content::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + } + Message::FocusAdjacent(direction) => { + if let Some(pane) = self.focus { + if let Some(adjacent) = + self.panes.adjacent(&pane, direction) + { + self.focus = Some(adjacent); + } + } + } + Message::Clicked(pane) => { + self.focus = Some(pane); + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.panes.resize(&split, ratio); + } + Message::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + self.panes.swap(&pane, &target); + } + Message::Dragged(_) => {} + Message::Close(pane) => { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } + } + Message::CloseFocused => { + if let Some(pane) = self.focus { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.is_command_pressed() => handle_hotkey(key_code), + _ => None, + } + }) + } + + fn view(&mut self) -> Element<Message> { + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&mut self.panes, |pane, content| { + let is_focused = focus == Some(pane); + + let title_bar = + pane_grid::TitleBar::new(format!("Pane {}", content.id)) + .padding(10) + .style(style::TitleBar { is_focused }); + + pane_grid::Content::new(content.view(pane, total_panes)) + .title_bar(title_bar) + .style(style::Pane { is_focused }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + Container::new(pane_grid) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .into() + } +} + +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { + use keyboard::KeyCode; + use pane_grid::{Axis, Direction}; + + let direction = match key_code { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + }; + + match key_code { + KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(Message::CloseFocused), + _ => direction.map(Message::FocusAdjacent), + } +} + +struct Content { + id: usize, + scroll: scrollable::State, + split_horizontally: button::State, + split_vertically: button::State, + close: button::State, +} + +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( + &mut self, + pane: pane_grid::Pane, + total_panes: usize, + ) -> 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(HorizontalAlignment::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 { + controls = controls.push(button( + close, + "Close", + Message::Close(pane), + style::Button::Destructive, + )); + } + + let content = Scrollable::new(scroll) + .width(Length::Fill) + .spacing(10) + .align_items(Align::Center) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() + } +} + +mod style { + 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 struct TitleBar { + pub is_focused: bool, + } + + impl container::StyleSheet for TitleBar { + fn style(&self) -> container::Style { + let pane = Pane { + is_focused: self.is_focused, + } + .style(); + + container::Style { + text_color: Some(Color::WHITE), + background: Some(pane.border_color.into()), + ..Default::default() + } + } + } + + pub struct Pane { + pub is_focused: bool, + } + + impl container::StyleSheet for Pane { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(SURFACE)), + border_width: 2.0, + border_color: if self.is_focused { + Color::BLACK + } else { + Color::from_rgb(0.7, 0.7, 0.7) + }, + ..Default::default() + } + } + } + + pub enum Button { + Primary, + Destructive, + } + + 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::Style { + text_color, + background: background.map(Background::Color), + border_radius: 5.0, + shadow_offset: Vector::new(0.0, 0.0), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + let background = match self { + Button::Primary => Some(HOVERED), + Button::Destructive => Some(Color { + a: 0.2, + ..active.text_color + }), + }; + + button::Style { + background: background.map(Background::Color), + ..active + } + } + } +} diff --git a/examples/pick_list/Cargo.toml b/examples/pick_list/Cargo.toml new file mode 100644 index 00000000..a87d7217 --- /dev/null +++ b/examples/pick_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pick_list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } diff --git a/examples/pick_list/README.md b/examples/pick_list/README.md new file mode 100644 index 00000000..6dc80bf4 --- /dev/null +++ b/examples/pick_list/README.md @@ -0,0 +1,18 @@ +## Pick-list + +A dropdown list of selectable options. + +It displays and positions an overlay based on the window position of the widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package pick_list +``` + +[`main`]: src/main.rs diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs new file mode 100644 index 00000000..68662602 --- /dev/null +++ b/examples/pick_list/src/main.rs @@ -0,0 +1,113 @@ +use iced::{ + pick_list, scrollable, Align, Container, Element, Length, PickList, + Sandbox, Scrollable, Settings, Space, Text, +}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + scroll: scrollable::State, + pick_list: pick_list::State<Language>, + selected_language: Language, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + LanguageSelected(Language), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Pick list - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::LanguageSelected(language) => { + self.selected_language = language; + } + } + } + + fn view(&mut self) -> Element<Message> { + let pick_list = PickList::new( + &mut self.pick_list, + &Language::ALL[..], + Some(self.selected_language), + Message::LanguageSelected, + ); + + let mut content = Scrollable::new(&mut self.scroll) + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push(Space::with_height(Length::Units(600))) + .push(Text::new("Which is your favorite language?")) + .push(pick_list); + + content = content.push(Space::with_height(Length::Units(600))); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Javascript, + Other, +} + +impl Language { + const ALL: [Language; 7] = [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Javascript, + Language::Other, + ]; +} + +impl Default for Language { + fn default() -> Language { + Language::Rust + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Javascript => "Javascript", + Language::Other => "Some other language", + } + ) + } +} diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 94320086..05e73992 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["image", "debug", "tokio"] } +iced = { path = "../..", features = ["image", "debug", "tokio_old"] } serde_json = "1.0" [dependencies.serde] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 4449b901..187e5dee 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -3,7 +3,7 @@ use iced::{ Container, Element, Image, Length, Row, Settings, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Pokedex::run(Settings::default()) } @@ -29,8 +29,9 @@ enum Message { impl Application for Pokedex { type Executor = iced::executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Pokedex, Command<Message>) { + fn new(_flags: ()) -> (Pokedex, Command<Message>) { ( Pokedex::Loading, Command::perform(Pokemon::search(), Message::PokemonFound), @@ -225,7 +226,7 @@ enum Error { impl From<reqwest::Error> for Error { fn from(error: reqwest::Error) -> Error { - dbg!(&error); + dbg!(error); Error::APIError } @@ -250,7 +251,7 @@ mod style { background: Some(Background::Color(match self { Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), })), - border_radius: 12, + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::WHITE, ..button::Style::default() diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index 43b09928..c9a8e798 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -1,6 +1,6 @@ use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; -pub fn main() { +pub fn main() -> iced::Result { Progress::run(Settings::default()) } @@ -36,12 +36,15 @@ impl Sandbox for Progress { 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, - )) + .push( + Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + ) + .step(0.01), + ) .into() } } diff --git a/examples/qr_code/Cargo.toml b/examples/qr_code/Cargo.toml new file mode 100644 index 00000000..7f2d4e42 --- /dev/null +++ b/examples/qr_code/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "qr_code" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["qr_code"] } diff --git a/examples/qr_code/README.md b/examples/qr_code/README.md new file mode 100644 index 00000000..2dd89c26 --- /dev/null +++ b/examples/qr_code/README.md @@ -0,0 +1,18 @@ +## QR Code Generator + +A basic QR code generator that showcases the `QRCode` widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/heavyexhaustedaracari"> + <img src="https://thumbs.gfycat.com/HeavyExhaustedAracari-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package qr_code +``` + +[`main`]: src/main.rs diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs new file mode 100644 index 00000000..37b4855d --- /dev/null +++ b/examples/qr_code/src/main.rs @@ -0,0 +1,81 @@ +use iced::qr_code::{self, QRCode}; +use iced::text_input::{self, TextInput}; +use iced::{ + Align, Column, Container, Element, Length, Sandbox, Settings, Text, +}; + +pub fn main() -> iced::Result { + QRGenerator::run(Settings::default()) +} + +#[derive(Default)] +struct QRGenerator { + data: String, + input: text_input::State, + qr_code: Option<qr_code::State>, +} + +#[derive(Debug, Clone)] +enum Message { + DataChanged(String), +} + +impl Sandbox for QRGenerator { + type Message = Message; + + fn new() -> Self { + QRGenerator { + qr_code: qr_code::State::new("").ok(), + ..Self::default() + } + } + + fn title(&self) -> String { + String::from("QR Code Generator - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::DataChanged(mut data) => { + data.truncate(100); + + self.qr_code = qr_code::State::new(&data).ok(); + self.data = data; + } + } + } + + fn view(&mut self) -> Element<Message> { + let title = Text::new("QR Code Generator") + .size(70) + .color([0.5, 0.5, 0.5]); + + let input = TextInput::new( + &mut self.input, + "Type the data of your QR code here...", + &self.data, + Message::DataChanged, + ) + .size(30) + .padding(15); + + let mut content = Column::new() + .width(Length::Units(700)) + .spacing(20) + .align_items(Align::Center) + .push(title) + .push(input); + + if let Some(qr_code) = self.qr_code.as_mut() { + content = content.push(QRCode::new(qr_code).cell_size(10)); + } + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(20) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml new file mode 100644 index 00000000..12753fb6 --- /dev/null +++ b/examples/scrollable/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "scrollable" +version = "0.1.0" +authors = ["Clark Moody <clark@clarkmoody.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/scrollable/README.md b/examples/scrollable/README.md new file mode 100644 index 00000000..ed0e31b5 --- /dev/null +++ b/examples/scrollable/README.md @@ -0,0 +1,15 @@ +# Scrollable +An example showcasing the various size and style options for the Scrollable. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +<div align="center"> + <a href="./screenshot.png"> + <img src="./screenshot.png" height="640px"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package scrollable +``` diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png Binary files differnew file mode 100644 index 00000000..2d800251 --- /dev/null +++ b/examples/scrollable/screenshot.png diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs new file mode 100644 index 00000000..8dd2e20c --- /dev/null +++ b/examples/scrollable/src/main.rs @@ -0,0 +1,184 @@ +mod style; + +use iced::{ + scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox, + Scrollable, Settings, Space, Text, +}; + +pub fn main() -> iced::Result { + ScrollableDemo::run(Settings::default()) +} + +struct ScrollableDemo { + theme: style::Theme, + variants: Vec<Variant>, +} + +#[derive(Debug, Clone)] +enum Message { + ThemeChanged(style::Theme), +} + +impl Sandbox for ScrollableDemo { + type Message = Message; + + fn new() -> Self { + ScrollableDemo { + theme: Default::default(), + variants: Variant::all(), + } + } + + fn title(&self) -> String { + String::from("Scrollable - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::ThemeChanged(theme) => self.theme = theme, + } + } + + fn view(&mut 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:")), + |column, option| { + column.push( + Radio::new( + *option, + &format!("{:?}", option), + Some(*theme), + Message::ThemeChanged, + ) + .style(*theme), + ) + }, + ); + + let scrollable_row = Row::with_children( + variants + .iter_mut() + .map(|variant| { + let mut scrollable = Scrollable::new(&mut variant.state) + .padding(10) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme) + .push(Text::new(variant.title)); + + if let Some(scrollbar_width) = variant.scrollbar_width { + scrollable = scrollable + .scrollbar_width(scrollbar_width) + .push(Text::new(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 + ))); + } + + if let Some(scroller_width) = variant.scroller_width { + scrollable = scrollable + .scroller_width(scroller_width) + .push(Text::new(format!( + "scroller_width: {:?}", + scroller_width + ))); + } + + scrollable = scrollable + .push(Space::with_height(Length::Units(100))) + .push(Text::new( + "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.")); + + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme) + .into() + }) + .collect(), + ) + .spacing(20) + .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); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .style(self.theme) + .into() + } +} + +/// A version of a scrollable +struct Variant { + title: &'static str, + state: scrollable::State, + scrollbar_width: Option<u16>, + scrollbar_margin: Option<u16>, + scroller_width: Option<u16>, +} + +impl Variant { + pub fn all() -> Vec<Self> { + vec![ + Self { + title: "Default Scrollbar", + state: scrollable::State::new(), + scrollbar_width: None, + scrollbar_margin: None, + scroller_width: None, + }, + Self { + title: "Slimmed & Margin", + state: scrollable::State::new(), + scrollbar_width: Some(4), + scrollbar_margin: Some(3), + scroller_width: Some(4), + }, + Self { + title: "Wide Scroller", + state: scrollable::State::new(), + scrollbar_width: Some(4), + scrollbar_margin: None, + scroller_width: Some(10), + }, + Self { + title: "Narrow Scroller", + state: scrollable::State::new(), + scrollbar_width: Some(10), + scrollbar_margin: None, + scroller_width: Some(4), + }, + ] + } +} diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs new file mode 100644 index 00000000..ae449141 --- /dev/null +++ b/examples/scrollable/src/style.rs @@ -0,0 +1,190 @@ +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 From<Theme> for Box<dyn container::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Container.into(), + } + } +} + +impl From<Theme> for Box<dyn radio::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Radio.into(), + } + } +} + +impl From<Theme> for Box<dyn scrollable::StyleSheet> { + 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, + } + } + + 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/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index c88cda50..44ced729 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } rand = "0.7" diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 4c239806..6a2de736 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,13 +7,14 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::{ - canvas, executor, Application, Canvas, Color, Command, Container, Element, - Length, Point, Settings, Size, Subscription, Vector, + canvas::{self, Cursor, Path, Stroke}, + executor, time, window, Application, Canvas, Color, Command, Element, + Length, Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; -pub fn main() { +pub fn main() -> iced::Result { SolarSystem::run(Settings { antialiasing: true, ..Settings::default() @@ -22,7 +23,6 @@ pub fn main() { struct SolarSystem { state: State, - solar_system: canvas::layer::Cache<State>, } #[derive(Debug, Clone, Copy)] @@ -33,12 +33,12 @@ enum Message { impl Application for SolarSystem { type Executor = executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Self, Command<Message>) { + fn new(_flags: ()) -> (Self, Command<Message>) { ( SolarSystem { state: State::new(), - solar_system: canvas::layer::Cache::new(), }, Command::none(), ) @@ -52,7 +52,6 @@ impl Application for SolarSystem { match message { Message::Tick(instant) => { self.state.update(instant); - self.solar_system.clear(); } } @@ -65,24 +64,20 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill) - .push(self.solar_system.with(&self.state)); - - Container::new(canvas) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } } #[derive(Debug)] struct State { + space_cache: canvas::Cache, + system_cache: canvas::Cache, + cursor_position: Point, start: Instant, - current: Instant, + now: Instant, stars: Vec<(Point, f32)>, } @@ -95,153 +90,125 @@ impl State { pub fn new() -> State { let now = Instant::now(); - let (width, height) = Settings::default().window.size; + let (width, height) = window::Settings::default().size; State { + space_cache: Default::default(), + system_cache: Default::default(), + cursor_position: Point::ORIGIN, start: now, - current: now, - stars: { - use rand::Rng; - - let mut rng = rand::thread_rng(); - - (0..100) - .map(|_| { - ( - Point::new( - rng.gen_range(0.0, width as f32), - rng.gen_range(0.0, height as f32), - ), - rng.gen_range(0.5, 1.0), - ) - }) - .collect() - }, + now, + stars: Self::generate_stars(width, height), } } pub fn update(&mut self, now: Instant) { - self.current = now; + self.now = now; + self.system_cache.clear(); + } + + fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + use rand::Rng; + + let mut rng = rand::thread_rng(); + + (0..100) + .map(|_| { + ( + Point::new( + rng.gen_range( + -(width as f32) / 2.0, + width as f32 / 2.0, + ), + rng.gen_range( + -(height as f32) / 2.0, + height as f32 / 2.0, + ), + ), + rng.gen_range(0.5, 1.0), + ) + }) + .collect() } } -impl canvas::Drawable for State { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::{Fill, Path, Stroke}; +impl<Message> canvas::Program<Message> for State { + fn draw( + &self, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; - let center = frame.center(); + let background = self.space_cache.draw(bounds.size(), |frame| { + let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let space = Path::new(|path| { - 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)); + } + }); - 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); }); - let sun = Path::new(|path| path.circle(center, Self::SUN_RADIUS)); - let orbit = Path::new(|path| path.circle(center, Self::ORBIT_RADIUS)); - - frame.fill(&space, Fill::Color(Color::BLACK)); - frame.fill(&stars, Fill::Color(Color::WHITE)); - frame.fill(&sun, Fill::Color(Color::from_rgb8(0xF9, 0xD7, 0x1C))); - frame.stroke( - &orbit, - Stroke { - width: 1.0, - color: Color::from_rgba8(0, 153, 255, 0.1), - ..Stroke::default() - }, - ); + let system = self.system_cache.draw(bounds.size(), |frame| { + let center = frame.center(); - let elapsed = self.current - self.start; - let elapsed_seconds = elapsed.as_secs() as f32; - let elapsed_millis = elapsed.subsec_millis() as f32; + let sun = Path::circle(center, Self::SUN_RADIUS); + let orbit = Path::circle(center, Self::ORBIT_RADIUS); - frame.with_save(|frame| { - frame.translate(Vector::new(center.x, center.y)); - frame.rotate( - (2.0 * PI / 60.0) * elapsed_seconds - + (2.0 * PI / 60_000.0) * elapsed_millis, + frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); + frame.stroke( + &orbit, + Stroke { + width: 1.0, + color: Color::from_rgba8(0, 153, 255, 0.1), + ..Stroke::default() + }, ); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - let earth = Path::new(|path| { - path.circle(Point::ORIGIN, Self::EARTH_RADIUS) - }); + let elapsed = self.now - self.start; + let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32 + + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; - let shadow = Path::new(|path| { - path.rectangle( + frame.with_save(|frame| { + frame.translate(Vector::new(center.x, center.y)); + frame.rotate(rotation); + frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + + let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); + let shadow = Path::rectangle( Point::new(0.0, -Self::EARTH_RADIUS), Size::new( Self::EARTH_RADIUS * 4.0, Self::EARTH_RADIUS * 2.0, ), - ) - }); + ); - frame.fill(&earth, Fill::Color(Color::from_rgb8(0x6B, 0x93, 0xD6))); + frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); - frame.with_save(|frame| { - frame.rotate( - ((2.0 * PI) / 6.0) * elapsed_seconds - + ((2.0 * PI) / 6_000.0) * elapsed_millis, - ); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); + frame.with_save(|frame| { + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - let moon = Path::new(|path| { - path.circle(Point::ORIGIN, Self::MOON_RADIUS) + let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); + frame.fill(&moon, Color::WHITE); }); - frame.fill(&moon, Fill::Color(Color::WHITE)); + frame.fill( + &shadow, + Color { + a: 0.7, + ..Color::BLACK + }, + ); }); - - frame.fill( - &shadow, - Fill::Color(Color { - a: 0.7, - ..Color::BLACK - }), - ); }); - } -} - -mod time { - use iced::futures; - use std::time::Instant; - pub fn every(duration: std::time::Duration) -> iced::Subscription<Instant> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = Instant; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| Instant::now()) - .boxed() - } + vec![background, system] } } diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml index 1dae3b83..075aa111 100644 --- a/examples/stopwatch/Cargo.toml +++ b/examples/stopwatch/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_futures = { path = "../../futures", features = ["async-std"] } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["tokio"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index d84c4817..983cf3e6 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,10 +1,11 @@ use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, + button, executor, time, Align, Application, Button, Column, Command, + Container, Element, HorizontalAlignment, Length, Row, Settings, + Subscription, Text, }; use std::time::{Duration, Instant}; -pub fn main() { +pub fn main() -> iced::Result { Stopwatch::run(Settings::default()) } @@ -28,10 +29,11 @@ enum Message { } impl Application for Stopwatch { - type Executor = iced_futures::executor::AsyncStd; + type Executor = executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Stopwatch, Command<Message>) { + fn new(_flags: ()) -> (Stopwatch, Command<Message>) { ( Stopwatch { duration: Duration::default(), @@ -142,43 +144,6 @@ impl Application for Stopwatch { } } -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<std::time::Instant> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = std::time::Instant; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| std::time::Instant::now()) - .boxed() - } - } -} - mod style { use iced::{button, Background, Color, Vector}; @@ -196,7 +161,7 @@ mod style { 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, + 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 d6f41b04..8975fd9a 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,10 +1,10 @@ use iced::{ button, scrollable, slider, text_input, Align, Button, Checkbox, Column, - Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, - Settings, Slider, Space, Text, TextInput, + Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox, + Scrollable, Settings, Slider, Space, Text, TextInput, }; -pub fn main() { +pub fn main() -> iced::Result { Styling::run(Settings::default()) } @@ -113,14 +113,17 @@ impl Sandbox for Styling { .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(Align::Center) .push(scrollable) + .push(Rule::vertical(38).style(self.theme)) .push(checkbox), ); @@ -136,8 +139,8 @@ impl Sandbox for Styling { mod style { use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, slider, - text_input, + button, checkbox, container, progress_bar, radio, rule, scrollable, + slider, text_input, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -228,18 +231,25 @@ mod style { } } + 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, Background, Color, Vector}; + use iced::{button, Color, Vector}; pub struct Button; impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.11, 0.42, 0.87, - ))), - border_radius: 12, + 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() @@ -258,8 +268,8 @@ mod style { mod dark { use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, - slider, text_input, Background, Color, + button, checkbox, container, progress_bar, radio, rule, scrollable, + slider, text_input, Color, }; const SURFACE: Color = Color::from_rgb( @@ -291,10 +301,8 @@ mod style { 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), + background: Color::from_rgb8(0x36, 0x39, 0x3F).into(), + text_color: Color::WHITE.into(), ..container::Style::default() } } @@ -305,16 +313,16 @@ mod style { impl radio::StyleSheet for Radio { fn active(&self) -> radio::Style { radio::Style { - background: Background::Color(SURFACE), + background: SURFACE.into(), dot_color: ACTIVE, - border_width: 1, + border_width: 1.0, border_color: ACTIVE, } } fn hovered(&self) -> radio::Style { radio::Style { - background: Background::Color(Color { a: 0.5, ..SURFACE }), + background: Color { a: 0.5, ..SURFACE }.into(), ..self.active() } } @@ -325,16 +333,16 @@ mod style { impl text_input::StyleSheet for TextInput { fn active(&self) -> text_input::Style { text_input::Style { - background: Background::Color(SURFACE), - border_radius: 2, - border_width: 0, + 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, + border_width: 1.0, border_color: ACCENT, ..self.active() } @@ -342,7 +350,7 @@ mod style { fn hovered(&self) -> text_input::Style { text_input::Style { - border_width: 1, + border_width: 1.0, border_color: Color { a: 0.3, ..ACCENT }, ..self.focused() } @@ -355,6 +363,10 @@ mod style { fn value_color(&self) -> Color { Color::WHITE } + + fn selection_color(&self) -> Color { + ACTIVE + } } pub struct Button; @@ -362,8 +374,8 @@ mod style { impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3, + background: ACTIVE.into(), + border_radius: 3.0, text_color: Color::WHITE, ..button::Style::default() } @@ -371,7 +383,7 @@ mod style { fn hovered(&self) -> button::Style { button::Style { - background: Some(Background::Color(HOVERED)), + background: HOVERED.into(), text_color: Color::WHITE, ..self.active() } @@ -379,7 +391,7 @@ mod style { fn pressed(&self) -> button::Style { button::Style { - border_width: 1, + border_width: 1.0, border_color: Color::WHITE, ..self.hovered() } @@ -391,14 +403,14 @@ mod style { impl scrollable::StyleSheet for Scrollable { fn active(&self) -> scrollable::Scrollbar { scrollable::Scrollbar { - background: Some(Background::Color(SURFACE)), - border_radius: 2, - border_width: 0, + background: SURFACE.into(), + border_radius: 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: ACTIVE, - border_radius: 2, - border_width: 0, + border_radius: 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } @@ -408,10 +420,7 @@ mod style { let active = self.active(); scrollable::Scrollbar { - background: Some(Background::Color(Color { - a: 0.5, - ..SURFACE - })), + background: Color { a: 0.5, ..SURFACE }.into(), scroller: scrollable::Scroller { color: HOVERED, ..active.scroller @@ -440,9 +449,9 @@ mod style { slider::Style { rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9 }, + shape: slider::HandleShape::Circle { radius: 9.0 }, color: ACTIVE, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } @@ -478,9 +487,9 @@ mod style { impl progress_bar::StyleSheet for ProgressBar { fn style(&self) -> progress_bar::Style { progress_bar::Style { - background: Background::Color(SURFACE), - bar: Background::Color(ACTIVE), - border_radius: 10, + background: SURFACE.into(), + bar: ACTIVE.into(), + border_radius: 10.0, } } } @@ -490,27 +499,38 @@ mod style { impl checkbox::StyleSheet for Checkbox { fn active(&self, is_checked: bool) -> checkbox::Style { checkbox::Style { - background: Background::Color(if is_checked { - ACTIVE - } else { - SURFACE - }), + background: if is_checked { ACTIVE } else { SURFACE } + .into(), checkmark_color: Color::WHITE, - border_radius: 2, - border_width: 1, + border_radius: 2.0, + border_width: 1.0, border_color: ACTIVE, } } fn hovered(&self, is_checked: bool) -> checkbox::Style { checkbox::Style { - background: Background::Color(Color { + background: Color { a: 0.8, ..if is_checked { ACTIVE } else { SURFACE } - }), + } + .into(), ..self.active(is_checked) } } } + + 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), + } + } + } } } diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml index 161ee6a8..d8f83ac2 100644 --- a/examples/svg/Cargo.toml +++ b/examples/svg/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../..", features = ["svg"] } -env_logger = "0.7" diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 811fdfb5..8707fa3b 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,19 +1,16 @@ -use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg}; - -pub fn main() { - env_logger::init(); +use iced::{Container, Element, Length, Sandbox, Settings, Svg}; +pub fn main() -> iced::Result { Tiger::run(Settings::default()) } -#[derive(Default)] struct Tiger; impl Sandbox for Tiger { type Message = (); fn new() -> Self { - Self::default() + Tiger } fn title(&self) -> String { @@ -23,18 +20,17 @@ impl Sandbox for Tiger { fn update(&mut self, _message: ()) {} fn view(&mut self) -> Element<()> { - let content = Column::new().padding(20).push( - Svg::new(format!( - "{}/resources/tiger.svg", - env!("CARGO_MANIFEST_DIR") - )) - .width(Length::Fill) - .height(Length::Fill), - ); - - Container::new(content) + let svg = Svg::from_path(format!( + "{}/resources/tiger.svg", + env!("CARGO_MANIFEST_DIR") + )) + .width(Length::Fill) + .height(Length::Fill); + + Container::new(svg) .width(Length::Fill) .height(Length::Fill) + .padding(20) .center_x() .center_y() .into() diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index f945cde5..c8926c33 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,13 +6,13 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["async-std"] } +iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" -directories = "2.0" +directories-next = "2.0" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Window", "Storage"] } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 7e866b19..ccee2703 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,7 +5,7 @@ use iced::{ }; use serde::{Deserialize, Serialize}; -pub fn main() { +pub fn main() -> iced::Result { Todos::run(Settings::default()) } @@ -40,8 +40,9 @@ enum Message { impl Application for Todos { type Executor = iced::executor::Default; type Message = Message; + type Flags = (); - fn new() -> (Todos, Command<Message>) { + fn new(_flags: ()) -> (Todos, Command<Message>) { ( Todos::Loading, Command::perform(SavedState::load(), Message::Loaded), @@ -424,7 +425,7 @@ impl Filter { } } -fn loading_message() -> Element<'static, Message> { +fn loading_message<'a>() -> Element<'a, Message> { Container::new( Text::new("Loading...") .horizontal_alignment(HorizontalAlignment::Center) @@ -436,7 +437,7 @@ fn loading_message() -> Element<'static, Message> { .into() } -fn empty_message(message: &str) -> Element<'static, Message> { +fn empty_message<'a>(message: &str) -> Element<'a, Message> { Container::new( Text::new(message) .width(Length::Fill) @@ -498,7 +499,7 @@ enum SaveError { impl SavedState { fn path() -> std::path::PathBuf { let mut path = if let Some(project_dirs) = - directories::ProjectDirs::from("rs", "Iced", "Todos") + directories_next::ProjectDirs::from("rs", "Iced", "Todos") { project_dirs.data_dir().into() } else { @@ -610,7 +611,7 @@ mod style { background: Some(Background::Color( Color::from_rgb(0.2, 0.2, 0.7), )), - border_radius: 10, + border_radius: 10.0, text_color: Color::WHITE, ..button::Style::default() } @@ -626,7 +627,7 @@ mod style { background: Some(Background::Color(Color::from_rgb( 0.8, 0.2, 0.2, ))), - border_radius: 5, + border_radius: 5.0, text_color: Color::WHITE, shadow_offset: Vector::new(1.0, 1.0), ..button::Style::default() diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 96749e90..bc7fac11 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["image", "debug"] } -env_logger = "0.7" +env_logger = "0.8" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 800254ed..e8755d39 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -4,7 +4,7 @@ use iced::{ Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, }; -pub fn main() { +pub fn main() -> iced::Result { env_logger::init(); Tour::run(Settings::default()) @@ -190,7 +190,7 @@ enum Step { Welcome, Slider { state: slider::State, - value: u16, + value: u8, }, RowsAndColumns { layout: Layout, @@ -222,13 +222,13 @@ enum Step { #[derive(Debug, Clone)] pub enum StepMessage { - SliderChanged(f32), + SliderChanged(u8), LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), + SpacingChanged(u16), + TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageWidthChanged(f32), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -249,12 +249,12 @@ impl<'a> Step { } StepMessage::SliderChanged(new_value) => { if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; + *value = new_value; } } StepMessage::TextSizeChanged(new_size) => { if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; + *size = new_size; } } StepMessage::TextColorChanged(new_color) => { @@ -269,12 +269,12 @@ impl<'a> Step { } StepMessage::SpacingChanged(new_spacing) => { if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; + *spacing = new_spacing; } } StepMessage::ImageWidthChanged(new_width) => { if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -384,7 +384,7 @@ impl<'a> Step { fn slider( state: &'a mut slider::State, - value: u16, + value: u8, ) -> Column<'a, StepMessage> { Self::container("Slider") .push(Text::new( @@ -397,8 +397,8 @@ impl<'a> Step { )) .push(Slider::new( state, - 0.0..=100.0, - value as f32, + 0..=100, + value, StepMessage::SliderChanged, )) .push( @@ -444,8 +444,8 @@ impl<'a> Step { .spacing(10) .push(Slider::new( spacing_slider, - 0.0..=80.0, - spacing as f32, + 0..=80, + spacing, StepMessage::SpacingChanged, )) .push( @@ -486,30 +486,25 @@ impl<'a> Step { ) .push(Slider::new( size_slider, - 10.0..=70.0, - size as f32, + 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 })); + let color_section = Column::new() .padding(20) .spacing(20) .push(Text::new("And its color:")) .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); + .push(color_sliders); Self::container("Text") .push(Text::new( @@ -530,7 +525,7 @@ impl<'a> Step { |choices, language| { choices.push(Radio::new( language, - language.into(), + language, selection, StepMessage::LanguageSelected, )) @@ -559,8 +554,8 @@ impl<'a> Step { .push(ferris(width)) .push(Slider::new( slider, - 100.0..=500.0, - width as f32, + 100..=500, + width, StepMessage::ImageWidthChanged, )) .push( @@ -694,7 +689,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { .center_x() } -fn button<'a, Message>( +fn button<'a, Message: Clone>( state: &'a mut button::State, label: &str, ) -> Button<'a, Message> { @@ -706,6 +701,17 @@ fn button<'a, Message>( .min_width(100) } +fn color_slider( + state: &mut slider::State, + component: f32, + update: impl Fn(f32) -> Color + 'static, +) -> Slider<f64, StepMessage> { + Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { Rust, @@ -729,16 +735,16 @@ impl Language { } } -impl From<Language> for &str { - fn from(language: Language) -> &'static str { - match language { +impl From<Language> for String { + fn from(language: Language) -> String { + String::from(match language { Language::Rust => "Rust", Language::Elm => "Elm", Language::Ruby => "Ruby", Language::Haskell => "Haskell", Language::C => "C", Language::Other => "Other", - } + }) } } @@ -763,7 +769,7 @@ mod style { 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, + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), ..button::Style::default() |