diff options
262 files changed, 13518 insertions, 2007 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 804273ff..3b0bf033 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,29 +15,15 @@ jobs: run: | echo "[profile.release]" >> Cargo.toml echo "lto = true" >> Cargo.toml - - name: Add .deb package metadata - run: | - echo '[package.metadata.deb.variants.todos]' >> Cargo.toml - echo 'extended-description = "A simple Todo app built with Iced, a cross-platform GUI library for Rust"' >> Cargo.toml - echo 'assets = [' >> Cargo.toml - echo '["target/release/examples/todos", "usr/bin/iced-todos", "755"],' >> Cargo.toml - echo '["iced-todos.desktop", "usr/share/applications/", "644"]' >> Cargo.toml - echo ']' >> Cargo.toml - - name: Create .desktop file - run: | - echo '[Desktop Entry]' >> iced-todos.desktop - echo 'Name=Todos - Iced' >> iced-todos.desktop - echo 'Exec=iced-todos' >> iced-todos.desktop - echo 'Type=Application' >> iced-todos.desktop - name: Build todos binary - run: cargo build --verbose --release --example todos + run: cargo build --verbose --release --package todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-unknown-linux-gnu - path: target/release/examples/todos - - name: Build todos .deb package - run: cargo deb --variant todos -- --example todos + path: target/release/todos + - name: Pack todos .deb package + run: cargo deb --no-build --package todos - name: Rename todos .deb package run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb - name: Archive todos .deb package @@ -61,12 +47,12 @@ jobs: echo '[target.x86_64-pc-windows-msvc]' >> .cargo/config echo 'rustflags = ["-Ctarget-feature=+crt-static"]' >> .cargo/config - name: Build todos binary - run: cargo build --verbose --release --example todos + run: cargo build --verbose --release --package todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-pc-windows-msvc - path: target/release/examples/todos.exe + path: target/release/todos.exe todos_macos: runs-on: macOS-latest @@ -80,9 +66,9 @@ jobs: - name: Build todos binary env: MACOSX_DEPLOYMENT_TARGET: 10.14 - run: cargo build --verbose --release --example todos + run: cargo build --verbose --release --package todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-apple-darwin - path: target/release/examples/todos + path: target/release/todos diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f911303..8c5ded3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: Test on: [push, pull_request] jobs: - all: + native: runs-on: ${{ matrix.os }} strategy: matrix: @@ -16,3 +16,14 @@ jobs: run: | cargo test --verbose --all cargo test --verbose --all --all-features + + web: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + targets: wasm32-unknown-unknown + - uses: actions/checkout@master + - name: Run checks + run: cargo check --package iced --target wasm32-unknown-unknown @@ -2,3 +2,4 @@ pkg/ **/*.rs.bk Cargo.lock +.cargo/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7d6826df..00000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly -matrix: - allow_failures: - - rust: nightly - fast_finish: true -before_install: - - | - if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then - sudo apt-get -qq update - sudo apt-get install -y libasound2-dev libudev-dev - fi @@ -12,8 +12,18 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +# Enables the `Image` widget +image = ["iced_wgpu/image"] +# Enables the `Svg` widget +svg = ["iced_wgpu/svg"] +# Enables the `Canvas` widget +canvas = ["iced_wgpu/canvas"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] +# Enables `tokio` as the `executor::Default` on native platforms +tokio = ["iced_futures/tokio"] +# Enables `async-std` as the `executor::Default` on native platforms +async-std = ["iced_futures/async-std"] [badges] maintenance = { status = "actively-developed" } @@ -21,24 +31,35 @@ maintenance = { status = "actively-developed" } [workspace] members = [ "core", + "futures", "native", + "style", "web", "wgpu", "winit", + "examples/bezier_tool", + "examples/clock", + "examples/counter", + "examples/custom_widget", + "examples/events", + "examples/geometry", + "examples/integration", + "examples/pokedex", + "examples/progress_bar", + "examples/solar_system", + "examples/stopwatch", + "examples/styling", + "examples/svg", + "examples/todos", + "examples/tour", ] +[dependencies] +iced_futures = { version = "0.1.0-alpha", path = "futures" } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_winit = { version = "0.1.0-alpha", path = "winit" } iced_wgpu = { version = "0.1.0", path = "wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_web = { version = "0.1.0", path = "web" } - -[dev-dependencies] -env_logger = "0.7" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -directories = "2.0" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen = "0.2.51" @@ -1,7 +1,7 @@ # Iced -[](https://travis-ci.org/hecrj/iced) +[](https://github.com/hecrj/iced/actions) [][documentation] -[](https://crates.io/crates/iced) +[](https://crates.io/crates/iced/0.1.0-beta) [](https://github.com/hecrj/iced/blob/master/LICENSE) [](https://iced.zulipchat.com) @@ -217,7 +217,7 @@ the [Rust Community Discord]. I go by `lone_scientist#9554` there. ## Sponsors The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] -[documentation]: https://docs.rs/iced +[documentation]: https://docs.rs/iced/0.1.0-beta/iced/ [examples]: https://github.com/hecrj/iced/tree/master/examples [Coffee]: https://github.com/hecrj/coffee [Elm]: https://elm-lang.org/ @@ -15,12 +15,16 @@ Once a step is completed, it is collapsed and added to this list: * [x] TodoMVC example ([#26]) * [x] Async actions ([#27]) * [x] Custom layout engine ([#52]) + * [x] Event subscriptions ([#122]) + * [x] Custom styling ([#146]) [#24]: https://github.com/hecrj/iced/issues/24 [#25]: https://github.com/hecrj/iced/issues/25 [#26]: https://github.com/hecrj/iced/issues/26 [#28]: https://github.com/hecrj/iced/issues/28 [#52]: https://github.com/hecrj/iced/pull/52 +[#122]: https://github.com/hecrj/iced/pull/122 +[#146]: https://github.com/hecrj/iced/pull/146 ### Multi-window support ([#27]) Open and control multiple windows at runtime. @@ -31,15 +35,6 @@ This approach should also allow us to perform custom optimizations for this part [#27]: https://github.com/hecrj/iced/issues/27 -### Event subscriptions ([#29]) -Besides performing async actions on demand, most applications also need to listen to events passively. An example of this could be a WebSocket connection, where messages can come in at any time. - -The idea here is to also follow [Elm]'s footsteps. We can add a method `subscriptions(&self) -> Subscription<Message>` to `Application` and keep the subscriptions alive in the runtime. - -The challenge here is designing the public API of subscriptions. Ideally, users should be able to create their own subscriptions and the GUI runtime should keep them alive by performing _subscription diffing_ (i.e. detecting when a subscription is added, changed, or removed). For this, we can take a look at [Elm] for inspiration. - -[#29]: https://github.com/hecrj/iced/issues/29 - ### Layers ([#30]) Currently, Iced assumes widgets cannot be laid out on top of each other. We should implement support for multiple layers of widgets. @@ -114,15 +109,6 @@ Once the state lifetime of widgets is removed, we could keep them alive between This is a big undertaking and introduces a new set of problems. We should research and consider the implications of this approach in detail before going for it. -### Improve style definitions -As of now, each widget defines its own styling options with methods, following the builder pattern. - -A unified way of defining and reusing styles would be great. I think we must avoid replicating CSS, we should try to stay as type-safe, explicit, and intuitive as possible. - -I think many different ideas in [`elm-ui`] could serve as an inspiration. - -[`elm-ui`]: https://www.youtube.com/watch?v=Ie-gqwSHQr0 - ### Try a different font rasterizer [`wgpu_glyph`] depends indirectly on [`rusttype`]. We may be able to gain performance by using a different font rasterizer. [`fontdue`], for instance, has reported noticeable speedups. diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 30050238..c0796e66 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `Color::from_rgb8` to easily build a `Color` from its hexadecimal representation. [#90] + +[#90]: https://github.com/hecrj/iced/pull/90 + ## [0.1.0] - 2019-11-25 ### Added diff --git a/core/Cargo.toml b/core/Cargo.toml index c623ba78..22bc7ceb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,9 +7,4 @@ description = "The essential concepts of Iced" license = "MIT" repository = "https://github.com/hecrj/iced" -[features] -# Exposes a future-based `Command` type -command = ["futures"] - [dependencies] -futures = { version = "0.3", optional = true } diff --git a/core/src/background.rs b/core/src/background.rs index 98047172..e1a37ddc 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -7,3 +7,9 @@ pub enum Background { Color(Color), // TODO: Add gradient and image variants } + +impl From<Color> for Background { + fn from(color: Color) -> Self { + Background::Color(color) + } +} diff --git a/core/src/color.rs b/core/src/color.rs index ec48c185..db509b88 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -25,6 +25,40 @@ impl Color { a: 1.0, }; + /// A color with no opacity. + pub const TRANSPARENT: Color = Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + + /// Creates a [`Color`] from its RGB components. + /// + /// [`Color`]: struct.Color.html + pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color { + Color { r, g, b, a: 1.0 } + } + + /// Creates a [`Color`] from its RGB8 components. + /// + /// [`Color`]: struct.Color.html + pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color { + Color::from_rgba8(r, g, b, 1.0) + } + + /// Creates a [`Color`] from its RGB8 components and an alpha value. + /// + /// [`Color`]: struct.Color.html + pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color { + Color { + r: f32::from(r) / 255.0, + g: f32::from(g) / 255.0, + b: f32::from(b) / 255.0, + a, + } + } + /// Converts the [`Color`] into its linear values. /// /// [`Color`]: struct.Color.html diff --git a/core/src/length.rs b/core/src/length.rs index 63ba6207..06d8cf0a 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -4,6 +4,15 @@ pub enum Length { /// Fill all the remaining space Fill, + /// Fill a portion of the remaining space relative to other elements. + /// + /// Let's say we have two elements: one with `FillPortion(2)` and one with + /// `FillPortion(3)`. The first will get 2 portions of the available space, + /// while the second one would get 3. + /// + /// `Length::Fill` is equivalent to `Length::FillPortion(1)`. + FillPortion(u16), + /// Fill the least amount of space Shrink, @@ -22,8 +31,15 @@ impl Length { pub fn fill_factor(&self) -> u16 { match self { Length::Fill => 1, + Length::FillPortion(factor) => *factor, Length::Shrink => 0, Length::Units(_) => 0, } } } + +impl From<u16> for Length { + fn from(units: u16) -> Self { + Length::Units(units) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 65304e8b..ea5e8b43 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,8 +12,8 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] mod align; mod background; @@ -22,6 +22,7 @@ mod font; mod length; mod point; mod rectangle; +mod size; mod vector; pub use align::{Align, HorizontalAlignment, VerticalAlignment}; @@ -31,10 +32,5 @@ pub use font::Font; pub use length::Length; pub use point::Point; pub use rectangle::Rectangle; +pub use size::Size; pub use vector::Vector; - -#[cfg(feature = "command")] -mod command; - -#[cfg(feature = "command")] -pub use command::Command; diff --git a/core/src/point.rs b/core/src/point.rs index 52307bba..b9a8149c 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -11,6 +11,11 @@ pub struct Point { } impl Point { + /// The origin (i.e. a [`Point`] with both X=0 and Y=0). + /// + /// [`Point`]: struct.Point.html + pub const ORIGIN: Point = Point::new(0.0, 0.0); + /// Creates a new [`Point`] with the given coordinates. /// /// [`Point`]: struct.Point.html @@ -19,6 +24,18 @@ impl Point { } } +impl From<[f32; 2]> for Point { + fn from([x, y]: [f32; 2]) -> Self { + Point { x, y } + } +} + +impl From<[u16; 2]> for Point { + fn from([x, y]: [u16; 2]) -> Self { + Point::new(x.into(), y.into()) + } +} + impl std::ops::Add<Vector> for Point { type Output = Self; diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index ee1e3807..7ed3d2df 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -27,6 +27,34 @@ impl Rectangle<f32> { && self.y <= point.y && point.y <= self.y + self.height } + + /// Computes the intersection with the given [`Rectangle`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + pub fn intersection( + &self, + other: &Rectangle<f32>, + ) -> Option<Rectangle<f32>> { + let x = self.x.max(other.x); + let y = self.y.max(other.y); + + let lower_right_x = (self.x + self.width).min(other.x + other.width); + let lower_right_y = (self.y + self.height).min(other.y + other.height); + + let width = lower_right_x - x; + let height = lower_right_y - y; + + if width > 0.0 && height > 0.0 { + Some(Rectangle { + x, + y, + width, + height, + }) + } else { + None + } + } } impl std::ops::Mul<f32> for Rectangle<u32> { @@ -41,3 +69,25 @@ impl std::ops::Mul<f32> for Rectangle<u32> { } } } + +impl From<Rectangle<u32>> for Rectangle<f32> { + fn from(rectangle: Rectangle<u32>) -> Rectangle<f32> { + Rectangle { + x: rectangle.x as f32, + y: rectangle.y as f32, + width: rectangle.width as f32, + height: rectangle.height as f32, + } + } +} + +impl From<Rectangle<f32>> for Rectangle<u32> { + fn from(rectangle: Rectangle<f32>) -> Rectangle<u32> { + Rectangle { + x: rectangle.x as u32, + y: rectangle.y as u32, + width: rectangle.width.ceil() as u32, + height: rectangle.height.ceil() as u32, + } + } +} diff --git a/native/src/size.rs b/core/src/size.rs index 30e2a57e..389b3247 100644 --- a/native/src/size.rs +++ b/core/src/size.rs @@ -37,3 +37,15 @@ impl Size { } } } + +impl From<[f32; 2]> for Size { + fn from([width, height]: [f32; 2]) -> Self { + Size { width, height } + } +} + +impl From<[u16; 2]> for Size { + fn from([width, height]: [u16; 2]) -> Self { + Size::new(width.into(), height.into()) + } +} diff --git a/core/src/vector.rs b/core/src/vector.rs index e0c5f073..4c1cbfab 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -31,3 +31,15 @@ where Self::new(self.x + b.x, self.y + b.y) } } + +impl<T> Default for Vector<T> +where + T: Default, +{ + fn default() -> Self { + Self { + x: T::default(), + y: T::default(), + } + } +} diff --git a/examples/README.md b/examples/README.md index 95ec6c5c..04399b93 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,11 +4,10 @@ you want to learn about a specific release, check out [the release list]. [the release list]: https://github.com/hecrj/iced/releases -## [Tour](tour.rs) - +## [Tour](tour) A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. -The __[`tour`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. +The __[`main`](tour/src/main.rs)__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. <div align="center"> <a href="https://gfycat.com/politeadorableiberianmole"> @@ -16,7 +15,6 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo </a> </div> -[`tour`]: tour.rs [`iced_winit`]: ../winit [`iced_native`]: ../native [`iced_wgpu`]: ../wgpu @@ -26,19 +24,17 @@ The __[`tour`]__ file contains all the code of the example! All the cross-platfo You can run the native version with `cargo run`: ``` -cargo run --example tour +cargo run --package tour ``` The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! [the usage instructions of `iced_web`]: ../web#usage +## [Todos](todos) +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. -## [Todos](todos.rs) - -A simple todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. - -All the example code is located in the __[`todos`]__ file. +The example code is located in the __[`main`](todos/src/main.rs)__ file. <div align="center"> <a href="https://gfycat.com/littlesanehalicore"> @@ -48,15 +44,54 @@ All the example code is located in the __[`todos`]__ file. You can run the native version with `cargo run`: ``` -cargo run --example todos +cargo run --package todos ``` We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! -[`todos`]: todos.rs [TodoMVC]: http://todomvc.com/ -## [Coffee] +## [Styling](styling) +An example showcasing custom styling with a light and dark theme. + +The example code is located in the __[`main`](styling/src/main.rs)__ file. + +<div align="center"> + <a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif"> + <img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px"> + </a> +</div> +You can run it with `cargo run`: +``` +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`]. +- [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. +- [`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. +- [`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. +- [`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. +- [`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. + +All of them are packaged in their own crate and, therefore, can be run using `cargo`: +``` +cargo run --package <example> +``` + +[`lyon`]: https://github.com/nical/lyon +[PokéAPI]: https://pokeapi.co/ +[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg + +## [Coffee] Since [Iced was born in May], it has been powering the user interfaces in [Coffee], an experimental 2D game engine. diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml new file mode 100644 index 00000000..b13a0aa5 --- /dev/null +++ b/examples/bezier_tool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bezier_tool" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } +lyon = "0.15" diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md new file mode 100644 index 00000000..933f2120 --- /dev/null +++ b/examples/bezier_tool/README.md @@ -0,0 +1,19 @@ +## Bézier tool + +A Paint-like tool for drawing Bézier curves using [`lyon`]. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/soulfulinfiniteantbear"> + <img src="https://thumbs.gfycat.com/SoulfulInfiniteAntbear-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +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 new file mode 100644 index 00000000..023eb0f7 --- /dev/null +++ b/examples/bezier_tool/src/main.rs @@ -0,0 +1,359 @@ +//! 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, + }; + use std::sync::Arc; + + 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: Arc::new(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; +use iced::{ + button, Align, Button, Column, Container, Element, Length, Sandbox, + Settings, Text, +}; + +pub fn main() { + Example::run(Settings { + antialiasing: true, + ..Settings::default() + }); +} + +#[derive(Default)] +struct Example { + bezier: bezier::State, + curves: Vec<bezier::Curve>, + button_state: button::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + AddCurve(bezier::Curve), + Clear, +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example::default() + } + + fn title(&self) -> String { + String::from("Bezier tool - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::AddCurve(curve) => { + self.curves.push(curve); + } + Message::Clear => { + self.bezier = bezier::State::default(); + self.curves.clear(); + } + } + } + + fn view(&mut self) -> Element<Message> { + let content = Column::new() + .padding(20) + .spacing(20) + .align_items(Align::Center) + .push( + Text::new("Bezier tool example") + .width(Length::Shrink) + .size(50), + ) + .push(Bezier::new( + &mut self.bezier, + self.curves.as_slice(), + Message::AddCurve, + )) + .push( + Button::new(&mut self.button_state, Text::new("Clear")) + .padding(8) + .on_press(Message::Clear), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml new file mode 100644 index 00000000..308cbfbb --- /dev/null +++ b/examples/clock/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "clock" +version = "0.1.0" +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" } +chrono = "0.4" +async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/clock/README.md b/examples/clock/README.md new file mode 100644 index 00000000..17509180 --- /dev/null +++ b/examples/clock/README.md @@ -0,0 +1,16 @@ +## Clock + +An application that uses the `Canvas` widget to draw a clock and its hands to display the current time. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <img src="https://user-images.githubusercontent.com/518289/74716344-a3e6b300-522e-11ea-8aea-3cc0a5100a2e.gif"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package clock +``` + +[`main`]: src/main.rs diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs new file mode 100644 index 00000000..d8266f06 --- /dev/null +++ b/examples/clock/src/main.rs @@ -0,0 +1,193 @@ +use iced::{ + canvas, executor, Application, Canvas, Color, Command, Container, Element, + Length, Point, Settings, Subscription, Vector, +}; + +pub fn main() { + Clock::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct Clock { + now: LocalTime, + clock: canvas::layer::Cache<LocalTime>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick(chrono::DateTime<chrono::Local>), +} + +impl Application for Clock { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Self, Command<Message>) { + ( + Clock { + now: chrono::Local::now().into(), + clock: canvas::layer::Cache::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Clock - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Tick(local_time) => { + let now = local_time.into(); + + if now != self.now { + self.now = now; + self.clock.clear(); + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + time::every(std::time::Duration::from_millis(500)).map(Message::Tick) + } + + fn view(&mut self) -> Element<Message> { + let canvas = Canvas::new() + .width(Length::Units(400)) + .height(Length::Units(400)) + .push(self.clock.with(&self.now)); + + Container::new(canvas) + .width(Length::Fill) + .height(Length::Fill) + .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 { + use chrono::Timelike; + + LocalTime { + hour: date_time.hour(), + minute: date_time.minute(), + second: date_time.second(), + } + } +} + +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 hour_and_minute_hands = canvas::Path::new(|path| { + path.move_to(center); + draw_hand(self.hour, 12, 0.5 * radius, offset, path); + + path.move_to(center); + draw_hand(self.minute, 60, 0.8 * radius, offset, path) + }); + + frame.stroke( + &hour_and_minute_hands, + canvas::Stroke { + width: 6.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) + }); + + frame.stroke( + &second_hand, + canvas::Stroke { + width: 3.0, + color: Color::WHITE, + line_cap: canvas::LineCap::Round, + ..canvas::Stroke::default() + }, + ); + } +} + +mod time { + use iced::futures; + + 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() + } + } +} diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml new file mode 100644 index 00000000..a763cd78 --- /dev/null +++ b/examples/counter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/counter/README.md b/examples/counter/README.md new file mode 100644 index 00000000..4d9fc5b9 --- /dev/null +++ b/examples/counter/README.md @@ -0,0 +1,18 @@ +## Counter + +The classic counter example explained in the [`README`](../../README.md). + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/fairdeadcatbird"> + <img src="https://thumbs.gfycat.com/FairDeadCatbird-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package counter +``` + +[`main`]: src/main.rs diff --git a/examples/counter.rs b/examples/counter/src/main.rs index b85db70d..bde0ea94 100644 --- a/examples/counter.rs +++ b/examples/counter/src/main.rs @@ -1,4 +1,4 @@ -use iced::{button, Button, Column, Element, Sandbox, Settings, Text}; +use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text}; pub fn main() { Counter::run(Settings::default()) @@ -25,7 +25,7 @@ impl Sandbox for Counter { } fn title(&self) -> String { - String::from("A simple counter") + String::from("Counter - Iced") } fn update(&mut self, message: Message) { @@ -42,6 +42,7 @@ impl Sandbox for Counter { fn view(&mut self) -> Element<Message> { Column::new() .padding(20) + .align_items(Align::Center) .push( Button::new(&mut self.increment_button, Text::new("Increment")) .on_press(Message::IncrementPressed), diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml new file mode 100644 index 00000000..30747dc0 --- /dev/null +++ b/examples/custom_widget/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_widget" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/custom_widget/README.md b/examples/custom_widget/README.md new file mode 100644 index 00000000..3d6cf902 --- /dev/null +++ b/examples/custom_widget/README.md @@ -0,0 +1,18 @@ +## Custom widget + +A demonstration of how to build a custom widget that draws a circle. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/jealouscornyhomalocephale"> + <img src="https://thumbs.gfycat.com/JealousCornyHomalocephale-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package custom_widget +``` + +[`main`]: src/main.rs diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs new file mode 100644 index 00000000..0a570745 --- /dev/null +++ b/examples/custom_widget/src/main.rs @@ -0,0 +1,145 @@ +//! This example showcases a simple native custom widget that draws a circle. +mod circle { + // 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::{ + layout, Background, Color, Element, Hasher, Layout, Length, + MouseCursor, Point, Size, Widget, + }; + use iced_wgpu::{Defaults, Primitive, Renderer}; + + pub struct Circle { + radius: u16, + } + + impl Circle { + pub fn new(radius: u16) -> Self { + Self { radius } + } + } + + impl<Message> Widget<Message, Renderer> for Circle { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(Size::new( + f32::from(self.radius) * 2.0, + f32::from(self.radius) * 2.0, + )) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.radius.hash(state); + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + ( + Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::BLACK), + border_radius: self.radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + MouseCursor::OutOfBounds, + ) + } + } + + impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use circle::Circle; +use iced::{ + slider, Align, Column, Container, Element, Length, Sandbox, Settings, + Slider, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + radius: u16, + slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + RadiusChanged(f32), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example { + radius: 50, + slider: slider::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom widget - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::RadiusChanged(radius) => { + self.radius = radius.round() as u16; + } + } + } + + fn view(&mut self) -> Element<Message> { + let content = Column::new() + .padding(20) + .spacing(20) + .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, + )); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml new file mode 100644 index 00000000..f883075f --- /dev/null +++ b/examples/events/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "events" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } diff --git a/examples/events/README.md b/examples/events/README.md new file mode 100644 index 00000000..3c9a1cab --- /dev/null +++ b/examples/events/README.md @@ -0,0 +1,18 @@ +## Events + +A log of native events displayed using a conditional `Subscription`. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/infamousicyermine"> + <img src="https://thumbs.gfycat.com/InfamousIcyErmine-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package events +``` + +[`main`]: src/main.rs diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs new file mode 100644 index 00000000..0c9dca05 --- /dev/null +++ b/examples/events/src/main.rs @@ -0,0 +1,86 @@ +use iced::{ + executor, Align, Application, Checkbox, Column, Command, Container, + Element, Length, Settings, Subscription, Text, +}; + +pub fn main() { + Events::run(Settings::default()) +} + +#[derive(Debug, Default)] +struct Events { + last: Vec<iced_native::Event>, + enabled: bool, +} + +#[derive(Debug, Clone)] +enum Message { + EventOccurred(iced_native::Event), + Toggled(bool), +} + +impl Application for Events { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Events, Command<Message>) { + (Events::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Events - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::EventOccurred(event) => { + self.last.push(event); + + if self.last.len() > 5 { + let _ = self.last.remove(0); + } + } + Message::Toggled(enabled) => { + self.enabled = enabled; + } + }; + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + if self.enabled { + iced_native::subscription::events().map(Message::EventOccurred) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element<Message> { + let events = self.last.iter().fold( + Column::new().spacing(10), + |column, event| { + column.push(Text::new(format!("{:?}", event)).size(40)) + }, + ); + + let toggle = Checkbox::new( + self.enabled, + "Listen to runtime events", + Message::Toggled, + ); + + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .push(events) + .push(toggle); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml new file mode 100644 index 00000000..9df52454 --- /dev/null +++ b/examples/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geometry" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/geometry/README.md b/examples/geometry/README.md new file mode 100644 index 00000000..4d5c81cb --- /dev/null +++ b/examples/geometry/README.md @@ -0,0 +1,18 @@ +## Geometry + +A custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../../wgpu). + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/activeunfitkangaroo"> + <img src="https://thumbs.gfycat.com/ActiveUnfitKangaroo-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package geometry +``` + +[`main`]: src/main.rs diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs new file mode 100644 index 00000000..795c6a71 --- /dev/null +++ b/examples/geometry/src/main.rs @@ -0,0 +1,213 @@ +//! This example showcases a simple native custom widget that renders using +//! arbitrary low-level geometry. +mod rainbow { + // 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::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, + Widget, + }; + use iced_wgpu::{ + triangle::{Mesh2D, Vertex2D}, + Defaults, Primitive, Renderer, + }; + + pub struct Rainbow; + + impl Rainbow { + pub fn new() -> Self { + Self + } + } + + impl<Message> Widget<Message, Renderer> for Rainbow { + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits.width(Length::Fill).resolve(Size::ZERO); + + layout::Node::new(Size::new(size.width, size.width)) + } + + fn hash_layout(&self, _state: &mut Hasher) {} + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let b = layout.bounds(); + + // R O Y G B I V + let color_r = [1.0, 0.0, 0.0, 1.0]; + let color_o = [1.0, 0.5, 0.0, 1.0]; + let color_y = [1.0, 1.0, 0.0, 1.0]; + let color_g = [0.0, 1.0, 0.0, 1.0]; + let color_gb = [0.0, 1.0, 0.5, 1.0]; + let color_b = [0.0, 0.2, 1.0, 1.0]; + let color_i = [0.5, 0.0, 1.0, 1.0]; + let color_v = [0.75, 0.0, 0.5, 1.0]; + + let posn_center = { + if b.contains(cursor_position) { + [cursor_position.x - b.x, cursor_position.y - b.y] + } else { + [b.width / 2.0, b.height / 2.0] + } + }; + + let posn_tl = [0.0, 0.0]; + let posn_t = [b.width / 2.0, 0.0]; + let posn_tr = [b.width, 0.0]; + let posn_r = [b.width, b.height / 2.0]; + let posn_br = [b.width, b.height]; + let posn_b = [(b.width / 2.0), b.height]; + let posn_bl = [0.0, b.height]; + let posn_l = [0.0, b.height / 2.0]; + + ( + Primitive::Mesh2D { + origin: Point::new(b.x, b.y), + buffers: std::sync::Arc::new(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, + ) + } + } + + impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use iced::{ + scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable, + Settings, Text, +}; +use rainbow::Rainbow; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + scroll: scrollable::State, +} + +impl Sandbox for Example { + type Message = (); + + fn new() -> Self { + Example { + scroll: scrollable::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom 2D geometry - Iced") + } + + fn update(&mut self, _: ()) {} + + fn view(&mut self) -> Element<()> { + let content = Column::new() + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Align::Start) + .push(Rainbow::new()) + .push(Text::new( + "In this example we draw a custom widget Rainbow, using \ + the Mesh2D primitive. This primitive supplies a list of \ + triangles, expressed as vertices and indices.", + )) + .push(Text::new( + "Move your cursor over it, and see the center vertex \ + follow you!", + )) + .push(Text::new( + "Every Vertex2D defines its own color. You could use the \ + Mesh2D primitive to render virtually any two-dimensional \ + geometry for your widget.", + )); + + let scrollable = Scrollable::new(&mut self.scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() + } +} diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml new file mode 100644 index 00000000..afc2c791 --- /dev/null +++ b/examples/integration/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "integration" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced_winit = { path = "../../winit" } +iced_wgpu = { path = "../../wgpu" } +env_logger = "0.7" diff --git a/examples/integration/README.md b/examples/integration/README.md new file mode 100644 index 00000000..d5aabc19 --- /dev/null +++ b/examples/integration/README.md @@ -0,0 +1,18 @@ +## Integration + +A demonstration of how to integrate Iced in an existing graphical application. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/nicemediocrekodiakbear"> + <img src="https://thumbs.gfycat.com/NiceMediocreKodiakbear-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package integration +``` + +[`main`]: src/main.rs diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs new file mode 100644 index 00000000..0457a058 --- /dev/null +++ b/examples/integration/src/controls.rs @@ -0,0 +1,102 @@ +use crate::Scene; + +use iced_wgpu::Renderer; +use iced_winit::{ + slider, Align, Color, Column, Element, Length, Row, Slider, Text, +}; + +pub struct Controls { + sliders: [slider::State; 3], +} + +#[derive(Debug)] +pub enum Message { + BackgroundColorChanged(Color), +} + +impl Controls { + pub fn new() -> Controls { + Controls { + sliders: Default::default(), + } + } + + pub fn update(&self, message: Message, scene: &mut Scene) { + match message { + Message::BackgroundColorChanged(color) => { + scene.background_color = color; + } + } + } + + pub fn view<'a>( + &'a mut self, + scene: &Scene, + ) -> Element<'a, Message, Renderer> { + let [r, g, b] = &mut self.sliders; + let background_color = scene.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| { + Message::BackgroundColorChanged(Color { + r, + ..background_color + }) + }, + )) + .push(Slider::new( + g, + 0.0..=1.0, + scene.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| { + Message::BackgroundColorChanged(Color { + b, + ..background_color + }) + }, + )); + + Row::new() + .width(Length::Fill) + .height(Length::Fill) + .align_items(Align::End) + .push( + Column::new() + .width(Length::Fill) + .align_items(Align::End) + .push( + Column::new() + .padding(10) + .spacing(10) + .push( + Text::new("Background color") + .color(Color::WHITE), + ) + .push(sliders) + .push( + Text::new(format!("{:?}", background_color)) + .size(14) + .color(Color::WHITE), + ), + ), + ) + .into() + } +} diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs new file mode 100644 index 00000000..4be913c1 --- /dev/null +++ b/examples/integration/src/main.rs @@ -0,0 +1,206 @@ +mod controls; +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 winit::{ + event::{DeviceEvent, Event, ModifiersState, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, +}; + +pub fn main() { + env_logger::init(); + + // 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 (mut device, mut queue) = + adapter.request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: wgpu::Limits::default(), + }); + + 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) + }; + 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 scene and GUI controls + let mut scene = Scene::new(&device); + let mut controls = Controls::new(); + + // Run event loop + event_loop.run(move |event, _, control_flow| { + // You should change this if you want to render continuosly + *control_flow = ControlFlow::Wait; + + match event { + Event::DeviceEvent { + event: DeviceEvent::ModifiersChanged(new_modifiers), + .. + } => { + modifiers = new_modifiers; + } + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::Resized(new_size) => { + logical_size = + new_size.to_logical(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, + window.scale_factor(), + modifiers, + ) { + events.push(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(), + &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()); + + // and request a redraw + window.request_redraw(); + } + Event::RedrawRequested(_) => { + if resized { + let size = window.inner_size(); + + swap_chain = SwapChain::new( + &device, + &surface, + format, + size.width, + size.height, + ); + } + + let (frame, viewport) = swap_chain.next_frame(); + + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { todo: 0 }, + ); + + // We draw the scene first + scene.draw(&mut encoder, &frame.view); + + // And then iced on top + let mouse_cursor = renderer.draw( + &mut device, + &mut encoder, + Target { + texture: &frame.view, + viewport, + }, + &output, + window.scale_factor(), + &["Some debug information!"], + ); + + // Then we submit the work + queue.submit(&[encoder.finish()]); + + // And update the mouse cursor + window.set_cursor_icon(iced_winit::conversion::mouse_cursor( + mouse_cursor, + )); + } + _ => {} + } + }) +} diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs new file mode 100644 index 00000000..efb1921b --- /dev/null +++ b/examples/integration/src/scene.rs @@ -0,0 +1,119 @@ +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); + + Scene { + background_color: Color::BLACK, + pipeline, + bind_group, + } + } + + pub fn draw( + &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(); + + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } + }, + }, + ], + depth_stencil_attachment: None, + }); + + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, &self.bind_group, &[]); + rpass.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"); + + 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 pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&bind_group_layout], + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + 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, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + (pipeline, bind_group) +} diff --git a/examples/integration/src/shader/frag.spv b/examples/integration/src/shader/frag.spv Binary files differnew file mode 100644 index 00000000..9d6807c9 --- /dev/null +++ b/examples/integration/src/shader/frag.spv diff --git a/examples/integration/src/shader/vert.spv b/examples/integration/src/shader/vert.spv Binary files differnew file mode 100644 index 00000000..0cabc9c0 --- /dev/null +++ b/examples/integration/src/shader/vert.spv diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml new file mode 100644 index 00000000..c1e3edb5 --- /dev/null +++ b/examples/pokedex/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pokedex" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["image", "debug", "tokio"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rand = { version = "0.7", features = ["wasm-bindgen"] } + +[dependencies.reqwest] +version = "0.10" +git = "https://github.com/hecrj/reqwest.git" +branch = "feature/wasm-deserialize-json" +features = ["json"] diff --git a/examples/pokedex/README.md b/examples/pokedex/README.md new file mode 100644 index 00000000..50720f57 --- /dev/null +++ b/examples/pokedex/README.md @@ -0,0 +1,17 @@ +# Pokédex +An application that loads a random Pokédex entry using the [PokéAPI]. + +All the example code can be found in the __[`main`](src/main.rs)__ file. + +<div align="center"> + <a href="https://gfycat.com/aggressivedarkelephantseal-rust-gui"> + <img src="https://thumbs.gfycat.com/AggressiveDarkElephantseal-small.gif" height="400px"> + </a> +</div> + +You can run it on native platforms with `cargo run`: +``` +cargo run --package pokedex +``` + +[PokéAPI]: https://pokeapi.co/ diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs new file mode 100644 index 00000000..4449b901 --- /dev/null +++ b/examples/pokedex/src/main.rs @@ -0,0 +1,260 @@ +use iced::{ + button, futures, image, Align, Application, Button, Column, Command, + Container, Element, Image, Length, Row, Settings, Text, +}; + +pub fn main() { + Pokedex::run(Settings::default()) +} + +#[derive(Debug)] +enum Pokedex { + Loading, + Loaded { + pokemon: Pokemon, + search: button::State, + }, + Errored { + error: Error, + try_again: button::State, + }, +} + +#[derive(Debug, Clone)] +enum Message { + PokemonFound(Result<Pokemon, Error>), + Search, +} + +impl Application for Pokedex { + type Executor = iced::executor::Default; + type Message = Message; + + fn new() -> (Pokedex, Command<Message>) { + ( + Pokedex::Loading, + Command::perform(Pokemon::search(), Message::PokemonFound), + ) + } + + fn title(&self) -> String { + let subtitle = match self { + Pokedex::Loading => "Loading", + Pokedex::Loaded { pokemon, .. } => &pokemon.name, + Pokedex::Errored { .. } => "Whoops!", + }; + + format!("{} - Pokédex", subtitle) + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::PokemonFound(Ok(pokemon)) => { + *self = Pokedex::Loaded { + pokemon, + search: button::State::new(), + }; + + Command::none() + } + Message::PokemonFound(Err(error)) => { + *self = Pokedex::Errored { + error, + try_again: button::State::new(), + }; + + Command::none() + } + Message::Search => match self { + Pokedex::Loading => Command::none(), + _ => { + *self = Pokedex::Loading; + + Command::perform(Pokemon::search(), Message::PokemonFound) + } + }, + } + } + + fn view(&mut self) -> Element<Message> { + let content = match self { + Pokedex::Loading => Column::new() + .width(Length::Shrink) + .push(Text::new("Searching for Pokémon...").size(40)), + Pokedex::Loaded { pokemon, search } => Column::new() + .max_width(500) + .spacing(20) + .align_items(Align::End) + .push(pokemon.view()) + .push( + button(search, "Keep searching!").on_press(Message::Search), + ), + Pokedex::Errored { try_again, .. } => Column::new() + .spacing(20) + .align_items(Align::End) + .push(Text::new("Whoops! Something went wrong...").size(40)) + .push(button(try_again, "Try again").on_press(Message::Search)), + }; + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +struct Pokemon { + number: u16, + name: String, + description: String, + image: image::Handle, +} + +impl Pokemon { + const TOTAL: u16 = 807; + + fn view(&self) -> Element<Message> { + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(Image::new(self.image.clone())) + .push( + Column::new() + .spacing(20) + .push( + Row::new() + .align_items(Align::Center) + .spacing(20) + .push( + Text::new(&self.name) + .size(30) + .width(Length::Fill), + ) + .push( + Text::new(format!("#{}", self.number)) + .size(20) + .color([0.5, 0.5, 0.5]), + ), + ) + .push(Text::new(&self.description)), + ) + .into() + } + + async fn search() -> Result<Pokemon, Error> { + use rand::Rng; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct Entry { + id: u32, + name: String, + flavor_text_entries: Vec<FlavorText>, + } + + #[derive(Debug, Deserialize)] + struct FlavorText { + flavor_text: String, + language: Language, + } + + #[derive(Debug, Deserialize)] + struct Language { + name: String, + } + + let id = { + let mut rng = rand::rngs::OsRng::default(); + + rng.gen_range(0, Pokemon::TOTAL) + }; + + let fetch_entry = async { + let url = + format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); + + reqwest::get(&url).await?.json().await + }; + + let (entry, image): (Entry, _) = + futures::future::try_join(fetch_entry, Self::fetch_image(id)) + .await?; + + let description = entry + .flavor_text_entries + .iter() + .filter(|text| text.language.name == "en") + .next() + .ok_or(Error::LanguageError)?; + + Ok(Pokemon { + number: id, + name: entry.name.to_uppercase(), + description: description + .flavor_text + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect(), + image, + }) + } + + async fn fetch_image(id: u16) -> Result<image::Handle, reqwest::Error> { + let url = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); + + #[cfg(not(target_arch = "wasm32"))] + { + let bytes = reqwest::get(&url).await?.bytes().await?; + + Ok(image::Handle::from_memory(bytes.as_ref().to_vec())) + } + + #[cfg(target_arch = "wasm32")] + Ok(image::Handle::from_path(url)) + } +} + +#[derive(Debug, Clone)] +enum Error { + APIError, + LanguageError, +} + +impl From<reqwest::Error> for Error { + fn from(error: reqwest::Error) -> Error { + dbg!(&error); + + Error::APIError + } +} + +fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { + Button::new(state, Text::new(text)) + .padding(10) + .style(style::Button::Primary) +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/progress_bar/Cargo.toml b/examples/progress_bar/Cargo.toml new file mode 100644 index 00000000..4eccbf14 --- /dev/null +++ b/examples/progress_bar/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "progress_bar" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/progress_bar/README.md b/examples/progress_bar/README.md new file mode 100644 index 00000000..1e927b3c --- /dev/null +++ b/examples/progress_bar/README.md @@ -0,0 +1,18 @@ +## Progress bar + +A simple progress bar that can be filled by using a slider. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/importantdevotedhammerheadbird"> + <img src="https://thumbs.gfycat.com/ImportantDevotedHammerheadbird-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package progress_bar +``` + +[`main`]: src/main.rs diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs new file mode 100644 index 00000000..43b09928 --- /dev/null +++ b/examples/progress_bar/src/main.rs @@ -0,0 +1,47 @@ +use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; + +pub fn main() { + Progress::run(Settings::default()) +} + +#[derive(Default)] +struct Progress { + value: f32, + progress_bar_slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SliderChanged(f32), +} + +impl Sandbox for Progress { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("A simple Progressbar") + } + + fn update(&mut self, message: Message) { + match message { + Message::SliderChanged(x) => self.value = x, + } + } + + fn view(&mut self) -> Element<Message> { + Column::new() + .padding(20) + .push(ProgressBar::new(0.0..=100.0, self.value)) + .push(Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + )) + .into() + } +} diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml new file mode 100644 index 00000000..c88cda50 --- /dev/null +++ b/examples/solar_system/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "solar_system" +version = "0.1.0" +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"] } +rand = "0.7" diff --git a/examples/solar_system/README.md b/examples/solar_system/README.md new file mode 100644 index 00000000..acfbc466 --- /dev/null +++ b/examples/solar_system/README.md @@ -0,0 +1,18 @@ +## Solar system + +An animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/selfassuredaromaticdunnart"> + <img src="https://thumbs.gfycat.com/SelfassuredAromaticDunnart-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package solar_system +``` + +[`main`]: src/main.rs diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs new file mode 100644 index 00000000..4c239806 --- /dev/null +++ b/examples/solar_system/src/main.rs @@ -0,0 +1,247 @@ +//! An animated solar system. +//! +//! This example showcases how to use a `Canvas` widget with transforms to draw +//! using different coordinate systems. +//! +//! Inspired by the example found in the MDN docs[1]. +//! +//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system +use iced::{ + canvas, executor, Application, Canvas, Color, Command, Container, Element, + Length, Point, Settings, Size, Subscription, Vector, +}; + +use std::time::Instant; + +pub fn main() { + SolarSystem::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct SolarSystem { + state: State, + solar_system: canvas::layer::Cache<State>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick(Instant), +} + +impl Application for SolarSystem { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Self, Command<Message>) { + ( + SolarSystem { + state: State::new(), + solar_system: canvas::layer::Cache::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Solar system - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Tick(instant) => { + self.state.update(instant); + self.solar_system.clear(); + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + time::every(std::time::Duration::from_millis(10)) + .map(|instant| Message::Tick(instant)) + } + + fn view(&mut self) -> Element<Message> { + let canvas = Canvas::new() + .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 { + start: Instant, + current: Instant, + stars: Vec<(Point, f32)>, +} + +impl State { + const SUN_RADIUS: f32 = 70.0; + const ORBIT_RADIUS: f32 = 150.0; + const EARTH_RADIUS: f32 = 12.0; + const MOON_RADIUS: f32 = 4.0; + const MOON_DISTANCE: f32 = 28.0; + + pub fn new() -> State { + let now = Instant::now(); + let (width, height) = Settings::default().window.size; + + State { + 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() + }, + } + } + + pub fn update(&mut self, now: Instant) { + self.current = now; + } +} + +impl canvas::Drawable for State { + fn draw(&self, frame: &mut canvas::Frame) { + use canvas::{Fill, Path, Stroke}; + use std::f32::consts::PI; + + let center = frame.center(); + + 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 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 elapsed = self.current - self.start; + let elapsed_seconds = elapsed.as_secs() as f32; + let elapsed_millis = elapsed.subsec_millis() as f32; + + 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.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + + let earth = Path::new(|path| { + path.circle(Point::ORIGIN, Self::EARTH_RADIUS) + }); + + let shadow = Path::new(|path| { + 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.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)); + + let moon = Path::new(|path| { + path.circle(Point::ORIGIN, Self::MOON_RADIUS) + }); + + frame.fill(&moon, Fill::Color(Color::WHITE)); + }); + + 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() + } + } +} diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml new file mode 100644 index 00000000..1dae3b83 --- /dev/null +++ b/examples/stopwatch/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stopwatch" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +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"] } diff --git a/examples/stopwatch/README.md b/examples/stopwatch/README.md new file mode 100644 index 00000000..4cf4582e --- /dev/null +++ b/examples/stopwatch/README.md @@ -0,0 +1,18 @@ +## Stopwatch + +A watch with start/stop and reset buttons showcasing how to listen to time. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/granularenviousgoitered-rust-gui"> + <img src="https://thumbs.gfycat.com/GranularEnviousGoitered-small.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package stopwatch +``` + +[`main`]: src/main.rs diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs new file mode 100644 index 00000000..d84c4817 --- /dev/null +++ b/examples/stopwatch/src/main.rs @@ -0,0 +1,206 @@ +use iced::{ + button, Align, Application, Button, Column, Command, Container, Element, + HorizontalAlignment, Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + Stopwatch::run(Settings::default()) +} + +struct Stopwatch { + duration: Duration, + state: State, + toggle: button::State, + reset: button::State, +} + +enum State { + Idle, + Ticking { last_tick: Instant }, +} + +#[derive(Debug, Clone)] +enum Message { + Toggle, + Reset, + Tick(Instant), +} + +impl Application for Stopwatch { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Stopwatch, Command<Message>) { + ( + Stopwatch { + duration: Duration::default(), + state: State::Idle, + toggle: button::State::new(), + reset: button::State::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Stopwatch - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Toggle => match self.state { + State::Idle => { + self.state = State::Ticking { + last_tick: Instant::now(), + }; + } + State::Ticking { .. } => { + self.state = State::Idle; + } + }, + Message::Tick(now) => match &mut self.state { + State::Ticking { last_tick } => { + self.duration += now - *last_tick; + *last_tick = now; + } + _ => {} + }, + Message::Reset => { + self.duration = Duration::default(); + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + match self.state { + State::Idle => Subscription::none(), + State::Ticking { .. } => { + time::every(Duration::from_millis(10)).map(Message::Tick) + } + } + } + + fn view(&mut self) -> Element<Message> { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + + let seconds = self.duration.as_secs(); + + let duration = Text::new(format!( + "{:0>2}:{:0>2}:{:0>2}.{:0>2}", + seconds / HOUR, + (seconds % HOUR) / MINUTE, + seconds % MINUTE, + self.duration.subsec_millis() / 10, + )) + .size(40); + + let button = |state, label, style| { + Button::new( + state, + Text::new(label) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .min_width(80) + .padding(10) + .style(style) + }; + + let toggle_button = { + let (label, color) = match self.state { + State::Idle => ("Start", style::Button::Primary), + State::Ticking { .. } => ("Stop", style::Button::Destructive), + }; + + button(&mut self.toggle, label, color).on_press(Message::Toggle) + }; + + let reset_button = + button(&mut self.reset, "Reset", style::Button::Secondary) + .on_press(Message::Reset); + + let controls = Row::new() + .spacing(20) + .push(toggle_button) + .push(reset_button); + + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .push(duration) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +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}; + + pub enum Button { + Primary, + Secondary, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/styling/Cargo.toml b/examples/styling/Cargo.toml new file mode 100644 index 00000000..eb729f93 --- /dev/null +++ b/examples/styling/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "styling" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/styling/README.md b/examples/styling/README.md new file mode 100644 index 00000000..6c198a54 --- /dev/null +++ b/examples/styling/README.md @@ -0,0 +1,15 @@ +# Styling +An example showcasing custom styling with a light and dark theme. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +<div align="center"> + <a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif"> + <img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package styling +``` diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs new file mode 100644 index 00000000..47408624 --- /dev/null +++ b/examples/styling/src/main.rs @@ -0,0 +1,515 @@ +use iced::{ + button, scrollable, slider, text_input, Align, Button, Checkbox, Column, + Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, + Settings, Slider, Space, Text, TextInput, +}; + +pub fn main() { + Styling::run(Settings::default()) +} + +#[derive(Default)] +struct Styling { + theme: style::Theme, + scroll: scrollable::State, + input: text_input::State, + input_value: String, + button: button::State, + slider: slider::State, + slider_value: f32, + toggle_value: bool, +} + +#[derive(Debug, Clone)] +enum Message { + ThemeChanged(style::Theme), + InputChanged(String), + ButtonPressed, + SliderChanged(f32), + CheckboxToggled(bool), +} + +impl Sandbox for Styling { + type Message = Message; + + fn new() -> Self { + Styling::default() + } + + fn title(&self) -> String { + String::from("Styling - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::ThemeChanged(theme) => self.theme = theme, + Message::InputChanged(value) => self.input_value = value, + Message::ButtonPressed => (), + Message::SliderChanged(value) => self.slider_value = value, + Message::CheckboxToggled(value) => self.toggle_value = value, + } + } + + fn view(&mut self) -> Element<Message> { + let choose_theme = style::Theme::ALL.iter().fold( + Column::new().spacing(10).push(Text::new("Choose a theme:")), + |column, theme| { + column.push( + Radio::new( + *theme, + &format!("{:?}", theme), + Some(self.theme), + Message::ThemeChanged, + ) + .style(self.theme), + ) + }, + ); + + let text_input = TextInput::new( + &mut self.input, + "Type something...", + &self.input_value, + Message::InputChanged, + ) + .padding(10) + .size(20) + .style(self.theme); + + let button = Button::new(&mut self.button, Text::new("Submit")) + .padding(10) + .on_press(Message::ButtonPressed) + .style(self.theme); + + let slider = Slider::new( + &mut self.slider, + 0.0..=100.0, + self.slider_value, + Message::SliderChanged, + ) + .style(self.theme); + + let progress_bar = + ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); + + let scrollable = Scrollable::new(&mut self.scroll) + .height(Length::Units(100)) + .style(self.theme) + .push(Text::new("Scroll me!")) + .push(Space::with_height(Length::Units(800))) + .push(Text::new("You did it!")); + + let checkbox = Checkbox::new( + self.toggle_value, + "Toggle me!", + Message::CheckboxToggled, + ) + .width(Length::Fill) + .style(self.theme); + + let content = Column::new() + .spacing(20) + .padding(20) + .max_width(600) + .push(choose_theme) + .push(Row::new().spacing(10).push(text_input).push(button)) + .push(slider) + .push(progress_bar) + .push( + Row::new() + .spacing(10) + .align_items(Align::Center) + .push(scrollable) + .push(checkbox), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .style(self.theme) + .into() + } +} + +mod style { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, slider, + text_input, + }; + + #[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 text_input::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::TextInput.into(), + } + } + } + + impl From<Theme> for Box<dyn button::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => light::Button.into(), + Theme::Dark => dark::Button.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 slider::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Slider.into(), + } + } + } + + impl From<Theme> for Box<dyn progress_bar::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::ProgressBar.into(), + } + } + } + + impl From<Theme> for Box<dyn checkbox::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Checkbox.into(), + } + } + } + + mod light { + use iced::{button, Background, 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, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } + } + + mod dark { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, + slider, text_input, Background, Color, + }; + + const SURFACE: Color = Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ); + + const ACCENT: Color = Color::from_rgb( + 0x6F as f32 / 255.0, + 0xFF as f32 / 255.0, + 0xE9 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, + ); + + pub struct Container; + + impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } + } + + pub struct Radio; + + impl radio::StyleSheet for Radio { + fn active(&self) -> radio::Style { + radio::Style { + background: Background::Color(SURFACE), + dot_color: ACTIVE, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self) -> radio::Style { + radio::Style { + background: Background::Color(Color { a: 0.5, ..SURFACE }), + ..self.active() + } + } + } + + pub struct TextInput; + + 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, + border_color: Color::TRANSPARENT, + } + } + + fn focused(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: ACCENT, + ..self.active() + } + } + + fn hovered(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: Color { a: 0.3, ..ACCENT }, + ..self.focused() + } + } + + fn placeholder_color(&self) -> Color { + Color::from_rgb(0.4, 0.4, 0.4) + } + + fn value_color(&self) -> Color { + Color::WHITE + } + } + + pub struct Button; + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + 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, + border_color: Color::WHITE, + ..self.hovered() + } + } + } + + pub struct Scrollable; + + impl scrollable::StyleSheet for Scrollable { + fn active(&self) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color(SURFACE)), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: ACTIVE, + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> scrollable::Scrollbar { + let active = self.active(); + + scrollable::Scrollbar { + background: Some(Background::Color(Color { + a: 0.5, + ..SURFACE + })), + scroller: scrollable::Scroller { + color: HOVERED, + ..active.scroller + }, + ..active + } + } + + fn dragging(&self) -> scrollable::Scrollbar { + let hovered = self.hovered(); + + scrollable::Scrollbar { + scroller: scrollable::Scroller { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..hovered.scroller + }, + ..hovered + } + } + } + + pub struct Slider; + + impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } + } + + pub struct ProgressBar; + + impl progress_bar::StyleSheet for ProgressBar { + fn style(&self) -> progress_bar::Style { + progress_bar::Style { + background: Background::Color(SURFACE), + bar: Background::Color(ACTIVE), + border_radius: 10, + } + } + } + + pub struct Checkbox; + + impl checkbox::StyleSheet for Checkbox { + fn active(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(if is_checked { + ACTIVE + } else { + SURFACE + }), + checkmark_color: Color::WHITE, + border_radius: 2, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(Color { + a: 0.8, + ..if is_checked { ACTIVE } else { SURFACE } + }), + ..self.active(is_checked) + } + } + } + } +} diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml new file mode 100644 index 00000000..161ee6a8 --- /dev/null +++ b/examples/svg/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "svg" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["svg"] } +env_logger = "0.7" diff --git a/examples/svg/README.md b/examples/svg/README.md new file mode 100644 index 00000000..9f53c177 --- /dev/null +++ b/examples/svg/README.md @@ -0,0 +1,17 @@ +## SVG + +An application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <img src="resources/tiger.svg"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package svg +``` + +[`main`]: src/main.rs +[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg diff --git a/examples/svg/resources/tiger.svg b/examples/svg/resources/tiger.svg new file mode 100644 index 00000000..679edec2 --- /dev/null +++ b/examples/svg/resources/tiger.svg @@ -0,0 +1,725 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg id="svg2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900" version="1.1"> + <g id="g4" fill="none" transform="matrix(1.7656463,0,0,1.7656463,324.90716,255.00942)"> + <g id="g6" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path8" d="m-122.3,84.285s0.1,1.894-0.73,1.875c-0.82-0.019-17.27-48.094-37.8-45.851,0,0,17.78-7.353,38.53,43.976z"/> + </g> + <g id="g10" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path12" d="m-118.77,81.262s-0.55,1.816-1.32,1.517c-0.77-0.298,0.11-51.104-19.95-55.978,0,0,19.22-0.864,21.27,54.461z"/> + </g> + <g id="g14" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path16" d="m-91.284,123.59s1.636,0.96,1.166,1.64c-0.471,0.67-49.642-12.13-59.102,6.23,0,0,3.68-18.89,57.936-7.87z"/> + </g> + <g id="g18" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path20" d="m-94.093,133.8s1.856,0.4,1.622,1.19c-0.233,0.79-50.939,4.13-54.129,24.53,0,0-2.46-19.08,52.507-25.72z"/> + </g> + <g id="g22" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path24" d="m-98.304,128.28s1.778,0.66,1.432,1.41-50.998-3.34-57.128,16.37c0,0,0.35-19.24,55.696-17.78z"/> + </g> + <g id="g26" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path28" d="m-109.01,110.07s1.31,1.38,0.67,1.9-44.38-25.336-58.53-10.29c0,0,8.74-17.147,57.86,8.39z"/> + </g> + <g id="g30" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path32" d="m-116.55,114.26s1.45,1.22,0.88,1.81c-0.58,0.59-46.97-20.148-59.32-3.6,0,0,6.74-18.023,58.44,1.79z"/> + </g> + <g id="g34" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path36" d="m-119.15,118.34s1.6,1,1.11,1.67c-0.49,0.66-49.27-13.56-59.25,4.51,0,0,4.22-18.77,58.14-6.18z"/> + </g> + <g id="g38" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path40" d="m-108.42,118.95s1.12,1.53,0.42,1.97c-0.7,0.43-40.77-30.818-56.73-17.71,0,0,10.87-15.884,56.31,15.74z"/> + </g> + <g id="g42" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path44" d="m-128.2,90s0.6,1.8-0.2,2-29.4-41.8-48.6-34.2c0,0,15.2-11.8,48.8,32.2z"/> + </g> + <g id="g46" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path48" d="m-127.5,96.979s0.97,1.629,0.23,1.996c-0.74,0.368-37.72-34.476-54.83-22.914,0,0,12.3-14.8,54.6,20.918z"/> + </g> + <g id="g50" stroke-width="0.17200001" stroke="#000" fill="#FFF"> + <path id="path52" d="m-127.62,101.35s1.12,1.53,0.42,1.97c-0.7,0.43-40.77-30.818-56.73-17.713,0,0,10.87-15.881,56.31,15.743z"/> + </g> + <g id="g54" stroke="#000" fill="#FFF"> + <path id="path56" d="m-129.83,103.06c0.5,6.05,1.49,12.62,3.23,15.74,0,0-3.6,12.4,5.2,25.6,0,0-0.4,7.2,1.2,10.4,0,0,4,8.4,8.8,9.2,3.88,0.65,12.607,3.72,22.468,5.12,0,0,17.132,14.08,13.932,26.88,0,0-0.4,16.4-4,18,0,0,11.6-11.2,2,5.6l-4.4,18.8s25.6-21.6,10-3.2l-10,26s19.6-18.4,12.4-10l-3.2,8.8s43.2-27.2,12.4,2.4c0,0,8-3.6,12.4-0.8,0,0,6.8-1.2,6,0.4,0,0-20.8,10.4-24.4,28.8,0,0,8.4-10,5.2,0.8l0.4,11.6s4-21.6,3.6,16c0,0,19.2-18,7.6,2.8v16.8s15.2-16.4,8.8-3.6c0,0,10-8.8,6,6.4,0,0-0.8,10.4,3.6-0.8,0,0,16-30.6,10-4.4,0,0-0.8,19.2,4,4.4,0,0,0.4,10.4,9.6,17.6,0,0-1.2-50.8,11.6-14.8l4,16.4s2.8-9.2,2.4-14.4l8,8s15.2-22.8,12-9.6c0,0-7.6,16-6,20.8,0,0,16.8-34.8,18-36.4,0,0-2,42.4,8.8,6.4,0,0,5.6,12,2.8,16.4,0,0,8-8,7.2-11.2,0,0,4.6-8.2,7.4,5.4,0,0,1.8,9.4,3.4,6.2,0,0,4,24,5.2,1.2,0,0,1.6-13.6-5.6-25.2,0,0,0.8-3.2-2-7.2,0,0,13.6,21.6,6.4-7.2,0,0,11.201,8,12.401,8,0,0-13.601-23.2-4.801-18.4,0,0-5.2-10.4,12.801,1.6,0,0-16.001-16,1.6-6.4,0,0,7.999,6.4,0.4-3.6,0,0-14.401-16,7.599,2,0,0,11.6,16.4,12.4,19.2,0,0-10-29.2-14.4-32,0,0,8.4-36.4,49.6-20.8,0,0,6.8,17.2,11.2-1.2,0,0,12.8-6.4,24,21.2,0,0,4-13.6,3.2-16.4,0,0,6.8,1.2,6,0,0,0,13.2,4.4,14.4,3.6,0,0,6.8,6.8,7.2,3.2,0,0,9.2,2.8,7.2-0.8,0,0,8.8,15.6,9.2,19.2l2.4-14,2,2.8s1.6-7.6,0.8-8.8,20,6.8,24.8,27.6l2,8.4s6-14.8,4.4-18.8c0,0,5.2,0.8,5.6,5.2,0,0,4-23.2-0.8-29.2,0,0,4.4-0.8,5.6,2.8v-7.2s7.2,0.8,7.2-1.6c0,0,4.4-4,6.4,0.8,0,0-12.4-35.2,6-16,0,0,7.2,10.8,3.6-8s-7.6-20.4-2.8-20.8c0,0,0.8-3.6-1.2-5.2s1.2,0,1.2,0,4.8,4-0.4-18c0,0,6.4,1.6-5.6-27.6,0,0,2.8-2.4-1.2-10.8,0,0,8,4.4,10.8,2.8,0,0-0.4-1.6-3.6-5.6,0,0-21.6-54.8-1.2-32.8,0,0,11.85,13.55,5.45-9.25,0,0-9.11-24.009-8.33-28.305l-429.55,23.015z"/> + </g> + <g id="g58" stroke="#000" fill="#cc7226"> + <path id="path60" d="m299.72,80.245c0.62,0.181,2.83,1.305,4.08,2.955,0,0,6.8,10.8,1.6-7.6,0,0-9.2-28.8-0.4-17.6,0,0,6,7.2,2.8-6.4-3.86-16.427-6.4-22.8-6.4-22.8s11.6,4.8-15.2-34.8l8.8,3.6s-19.6-39.6-41.2-44.8l-8-6s38.4-38,25.6-74.8c0,0-6.8-5.2-16.4,4,0,0-6.4,4.8-12.4,3.2,0,0-30.8,1.2-32.8,1.2s-36.8-37.2-102.4-19.6c0,0-5.2,2-9.599,0.8,0,0-18.401-16-67.201,6.8,0,0-10,2-11.6,2s-4.4,0-12.4,6.4-8.4,7.2-10.4,8.8c0,0-16.4,11.2-21.2,12,0,0-11.6,6.4-16,16.4l-3.6,1.2s-1.6,7.2-2,8.4c0,0-4.8,3.6-5.6,9.2,0,0-8.8,6-8.4,10.4,0,0-1.6,5.2-2.4,10,0,0-7.2,4.8-6.4,7.6,0,0-7.6,14-6.4,20.8,0,0-6.4-0.4-9.2,2,0,0-0.8,4.8-2.4,5.2,0,0-2.8,1.2-0.4,5.2,0,0-1.6,2.8-2,4.4,0,0,0.8,2.8-3.6,8.4,0,0-6.4,18.8-4.4,24,0,0,0.4,4.8-2.4,6.4,0,0-3.6-0.4,4.8,11.6,0,0,0.8,1.2-2.4,3.6,0,0-17.2,3.6-19.6,20,0,0-13.6,14.8-13.6,20,0,2.305,0.27,5.452,0.97,10.06,0,0-0.57,8.34,27.03,9.14s402.72-31.355,402.72-31.355z"/> + </g> + <g id="g62" fill="#cc7226"> + <path id="path64" d="m-115.6,102.6c-25-39.4-10.6,17-10.6,17,8.8,34.4,138.4-3.2,138.4-3.2s168.8-30.4,180-34.4,106.4,2.4,106.4,2.4l-5.6-16.8c-64.8-46.4-84-23.2-97.6-27.2s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2-31.74-22.951-16.8,8.8c16,34-58.4,39.2-75.2,28s7.2,18.4,7.2,18.4c18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4-8.8-2.4-8.31-23.101-40,3.2c-20,16.6-33.8-5.4-33.8-5.4l-2.8,11.6z"/> + </g> + <g id="g66" fill="#e87f3a"> + <path id="path68" d="m133.51,25.346c-6.4,0.8-31.77-22.939-16.8,8.8,16.6,35.2-58.4,39.2-75.2,28-16.801-11.2,7.2,18.4,7.2,18.4,18.4,20.004-16.001,3.2-16.001,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4.004-8.8-2.4c-0.8-6.4-8.179-22.934-40,3.2-21.236,17.344-34.729-4.109-34.729-4.109l-3.2,10.113c-25-39.804-9.93,18.51-9.93,18.51,8.81,34.4,139.06-4.51,139.06-4.51s168.8-30.404,180-34.404,105.53,2.327,105.53,2.327l-5.53-17.309c-64.8-46.4-83.2-22.618-96.8-26.618s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g70" fill="#ea8c4d"> + <path id="path72" d="m134.82,27.091c-6.4,0.8-31.14-23.229-16.8,8.8,16.2,36.201-58.401,39.201-75.201,28.001s7.2,18.4,7.2,18.4c18.4,19.998-16,3.2-16,3.2-34.4-12.8-58.401,12.8-61.601,13.6s-8,3.998-8.8-2.4c-0.8-6.4-8.048-22.767-40,3.2-22.473,18.088-35.658-2.818-35.658-2.818l-3.6,8.616c-23.8-38.998-9.25,20.02-9.25,20.02,8.8,34.4,139.71-5.82,139.71-5.82s168.8-30.398,180-34.398,104.65,2.254,104.65,2.254l-5.45-17.818c-64.8-46.4-82.4-22.037-96-26.037s-11.2,5.6-14.4,6.401c-3.2,0.8-42.4-24.001-48.8-23.201z"/> + </g> + <g id="g74" fill="#ec9961"> + <path id="path76" d="m136.13,28.837c-6.4,0.8-31.13-23.232-16.8,8.8,16.8,37.556-58.936,38.845-75.202,28-16.8-11.2,7.2,18.4,7.2,18.4,18.4,20.003-16,3.2-16,3.2-34.4-12.8-58.4,12.803-61.6,13.603s-8,4-8.8-2.403c-0.8-6.4-7.917-22.598-40.001,3.203-23.709,18.83-36.587-1.53-36.587-1.53l-4,7.13c-21.8-36.803-8.58,21.52-8.58,21.52,8.8,34.4,140.37-7.12,140.37-7.12s168.8-30.403,180-34.403,103.78,2.182,103.78,2.182l-5.38-18.327c-64.8-46.401-81.6-21.455-95.2-25.455s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g78" fill="#eea575"> + <path id="path80" d="m137.44,30.583c-6.4,0.8-30.63-23.454-16.8,8.8,16.8,39.2-58.403,39.2-75.203,28s7.2,18.4,7.2,18.4c18.4,19.997-16,3.2-16,3.2-34.4-12.8-58.4,12.797-61.6,13.597s-8,4-8.8-2.4c-0.8-6.397-7.785-22.428-40,3.2-24.946,19.58-37.507-0.23-37.507-0.23l-4.4,5.63c-19.8-34.798-7.91,23.04-7.91,23.04,8.8,34.4,141.02-8.44,141.02-8.44s168.8-30.397,180-34.397,102.91,2.109,102.91,2.109l-5.31-18.837c-64.8-46.4-80.8-20.872-94.4-24.872s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g82" fill="#f1b288"> + <path id="path84" d="m138.75,32.328c-6.4,0.8-32.37-22.651-16.8,8.8,19.2,38.8-58.404,39.2-75.204,28s7.2,18.4,7.2,18.4c18.4,20.002-16,3.2-16,3.2-34.4-12.8-58.4,12.802-61.6,13.602s-8,4-8.8-2.4c-0.8-6.402-7.654-22.265-40,3.2-26.182,20.33-38.436,1.05-38.436,1.05l-4.8,4.15c-18-33.202-7.24,24.54-7.24,24.54,8.8,34.4,141.68-9.74,141.68-9.74s168.8-30.402,180-34.402,102.03,2.036,102.03,2.036l-5.23-19.345c-64.8-46.4-80-20.291-93.6-24.291s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g86" fill="#f3bf9c"> + <path id="path88" d="m140.06,34.073c-6.4,0.8-32.75-22.46-16.8,8.8,20.4,40.001-58.405,39.201-75.205,28.001s7.2,18.4,7.2,18.4c18.4,19.996-16,3.2-16,3.2-34.4-12.8-58.4,12.796-61.6,13.596s-8,4-8.8-2.4c-0.8-6.396-7.523-22.092-40,3.2-27.419,21.08-39.365,2.35-39.365,2.35l-5.2,2.65c-16-30.196-6.56,26.06-6.56,26.06,8.8,34.4,142.32-11.06,142.32-11.06s168.8-30.396,180-34.396,101.16,1.963,101.16,1.963l-5.16-19.854c-64.8-46.4-79.2-19.709-92.8-23.709-13.6-4.001-11.2,5.6-14.4,6.4s-42.4-24.001-48.8-23.201z"/> + </g> + <g id="g90" fill="#f5ccb0"> + <path id="path92" d="m141.36,35.819c-6.4,0.8-33.84-21.875-16.8,8.8,22,39.6-58.396,39.2-75.196,28s7.2,18.4,7.2,18.4c18.4,20.001-16,3.2-16,3.2-34.4-12.8-58.4,12.801-61.6,13.601s-8,4-8.8-2.4c-0.8-6.401-7.391-21.928-40,3.2-28.655,21.82-40.294,3.64-40.294,3.64l-5.6,1.16c-14.4-28.401-5.89,27.56-5.89,27.56,8.8,34.4,142.98-12.36,142.98-12.36s168.8-30.401,180-34.401,100.3,1.891,100.3,1.891l-5.1-20.364c-64.8-46.4-78.4-19.127-92-23.127s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g94" fill="#f8d8c4"> + <path id="path96" d="m142.67,37.565c-6.4,0.8-33.84-21.876-16.8,8.8,22,39.6-58.396,39.2-75.196,28s7.2,18.4,7.2,18.4c18.4,19.995-16,3.2-16,3.2-34.401-12.8-58.401,12.795-61.601,13.595s-8,4-8.8-2.4-7.259-21.755-40,3.2c-29.891,22.57-41.213,4.93-41.213,4.93l-6-0.33c-13.61-26.396-5.22,29.08-5.22,29.08,8.8,34.4,143.63-13.68,143.63-13.68s168.8-30.395,180-34.395,99.42,1.818,99.42,1.818l-5.01-20.873c-64.81-46.4-77.61-18.545-91.21-22.545s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g98" fill="#fae5d7"> + <path id="path100" d="m143.98,39.31c-6.4,0.8-33.45-22.087-16.8,8.8,22,40.8-58.397,39.2-75.197,28s7.2,18.4,7.2,18.4c18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6-3.201,0.8-8.001,4-8.801-2.4s-7.128-21.592-40,3.2c-31.127,23.31-42.142,6.22-42.142,6.22l-6.4-1.82c-13-24-4.55,30.58-4.55,30.58,8.8,34.4,144.29-14.98,144.29-14.98s168.8-30.4,180-34.4,98.55,1.746,98.55,1.746l-4.95-21.382c-64.8-46.401-76.8-17.964-90.4-21.964s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2z"/> + </g> + <g id="g102" fill="#fcf2eb"> + <path id="path104" d="m145.29,41.055c-6.4,0.8-32.37-22.644-16.8,8.8,21.2,42.801-58.398,39.201-75.198,28.001s7.2,18.4,7.2,18.4c18.4,20.004-16,3.2-16,3.2-34.4-12.8-58.4,12.804-61.6,13.604s-8,4-8.8-2.4-6.997-21.428-40,3.2c-32.365,24.05-43.072,7.5-43.072,7.5l-6.8-3.3c-12.8-23.204-3.87,32.09-3.87,32.09,8.8,34.4,144.94-16.29,144.94-16.29s168.8-30.4,180-34.404c11.2-4,97.67,1.674,97.67,1.674l-4.87-21.893c-64.8-46.4-76-17.381-89.6-21.381-13.6-4.001-11.2,5.6-14.4,6.4s-42.4-24.001-48.8-23.201z"/> + </g> + <g id="g106" fill="#FFF"> + <path id="path108" d="m-115.8,119.6c-12.8-22-3.2,33.6-3.2,33.6,8.8,34.4,145.6-17.6,145.6-17.6s168.8-30.4,180-34.4,96.8,1.6,96.8,1.6l-4.8-22.4c-64.8-46.4-75.2-16.8-88.8-20.8s-11.2,5.6-14.4,6.4-42.4-24-48.8-23.2-31.62-23.007-16.8,8.8c22.23,47.707-60.759,37.627-75.2,28-16.8-11.2,7.2,18.4,7.2,18.4,18.4,20-16,3.2-16,3.2-34.4-12.8-58.4,12.8-61.6,13.6s-8,4-8.8-2.4-6.865-21.256-40,3.2c-33.6,24.8-44,8.8-44,8.8l-7.2-4.8z"/> + </g> + <g id="g110" fill="#000"> + <path id="path112" d="m-74.2,149.6s-7.2,11.6,13.6,24.8c0,0,1.4,1.4-16.6-2.8,0,0-6.2-2-7.8-12.4,0,0-4.8-4.4-9.6-10s20.4,0.4,20.4,0.4z"/> + </g> + <g id="g114" fill="#CCC"> + <path id="path116" d="m65.8,102s17.698,26.82,17.1,31.6c-1.3,10.4-1.5,20,1.7,24,3.201,4,12.001,37.2,12.001,37.2s-0.4,1.2,11.999-36.8c0,0,11.6-16-8.4-34.4,0,0-35.2-28.8-34.4-21.6z"/> + </g> + <g id="g118" fill="#000"> + <path id="path120" d="m-54.2,176.4s11.2,7.2-3.2,38.4l6.4-2.4s-0.8,11.2-4,13.6l7.2-3.2s4.8,8,0.8,12.8c0,0,16.8,8,16,14.4,0,0,6.4-8,2.4-14.4s-11.2-2.4-10.4-20.8l-8.8,3.2s5.6-8.8,5.6-15.2l-8,2.4s15.469-26.58,4.8-28c-6-0.8-8.8-0.8-8.8-0.8z"/> + </g> + <g id="g122" fill="#CCC"> + <path id="path124" d="m-21.8,193.2s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/> + </g> + <g id="g126" fill="#CCC"> + <path id="path128" d="m-11.4,201.2s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/> + </g> + <g id="g130" fill="#CCC"> + <path id="path132" d="m1.8,186s2.8-4.4,0-3.6-34,15.6-40,25.2c0,0,34.4-24.4,40-21.6z"/> + </g> + <g id="g134" fill="#CCC"> + <path id="path136" d="m-21.4,229.6s0-6-2.8-5.2-38.8,18.4-44.8,28c0,0,42-25.6,47.6-22.8z"/> + </g> + <g id="g138" fill="#CCC"> + <path id="path140" d="m-20.2,218.8s1.2-4.8-1.6-4c-2,0-28.4,11.6-34.4,21.2,0,0,29.6-21.6,36-17.2z"/> + </g> + <g id="g142" fill="#CCC"> + <path id="path144" d="m-34.6,266.4-10,7.6s10.4-7.6,14-6.4c0,0-6.8,11.2-7.6,16.4,0,0,10.4-12.8,16-12.4,0,0,7.6,0.4,7.6,11.2,0,0,5.6-10.4,8.8-10,0,0,1.2,6.4,0,13.2,0,0,4-7.6,8-6,0,0,6.4-2,5.6,9.6,0,0,0,10.4-0.8,13.2,0,0,5.6-26.4,8-26.8,0,0,8-1.2,12.8,7.6,0,0-4-7.6,0.8-5.6,0,0,10.8,1.6,14,8.4,0,0-6.8-12-1.2-8.8l8,6.4s8.4,21.2,10.4,22.8c0,0-7.6-21.6-6-21.6,0,0-2-12,3.2,2.8,0,0-3.2-14,2.4-13.2s10,10.8,18.4,8.4c0,0,9.601,5.6,11.601-63.6l-124,46.8z"/> + </g> + <g id="g146" fill="#000"> + <path id="path148" d="m-29.8,173.6s14.8-6,54.8,0c0,0,7.2,0.4,14-8.4s33.6-16,40-14l9.601,6.4,0.8,1.2s12.399,10.4,12.799,18-14.399,55.6-24,71.6c-9.6,16-19.2,28.4-38.4,26,0,0-20.8-4-46.4,0,0,0-29.2-1.6-32-9.6s11.2-23.2,11.2-23.2,4.4-8.4,3.2-22.8-0.8-42.4-5.6-45.2z"/> + </g> + <g id="g150" fill="#e5668c"> + <path id="path152" d="M-7.8,175.6c8.4,18.4-21.2,83.6-21.2,83.6-2,1.6,12.66,7.65,22.8,5.2,10.946-2.64,51.2,1.6,51.2,1.6,23.6-15.6,36.4-60,36.4-60s10.401-24-7.2-27.2c-17.6-3.2-82-3.2-82-3.2z"/> + </g> + <g id="g154" fill="#b23259"> + <path id="path156" d="m-9.831,206.5c3.326-12.79,4.91-24.59,2.031-30.9,0,0,62.4,6.4,73.6-14.4,4.241-7.87,19.001,22.8,18.6,32.4,0,0-63,14.4-77.8,3.2l-16.431,9.7z"/> + </g> + <g id="g158" fill="#a5264c"> + <path id="path160" d="m-5.4,222.8s2,7.2-0.4,11.2c0,0-1.6,0.8-2.8,1.2,0,0,1.2,3.6,7.2,5.2,0,0,2,4.4,4.4,4.8s7.2,6,11.2,4.8,15.2-5.2,15.2-5.2,5.6-3.2,14.4,0.4c0,0,2.375-0.8,2.8-4.8,0.5-4.7,3.6-8.4,5.6-10.4s11.6-14.8,10.4-15.2-68,8-68,8z"/> + </g> + <g id="g162" stroke="#000" fill="#ff727f"> + <path id="path164" d="m-9.8,174.4s-2.8,22.4,0.4,30.8,2.4,10.4,1.6,14.4,3.6,14,9.2,20l12,1.6s15.2-3.6,24.4-0.8c0,0,8.994,1.34,12.4-13.6,0,0,4.8-6.4,12-9.2s14.4-44.4,10.4-52.4-18.4-12.4-34.4,3.2-18-1.2-48,6z"/> + </g> + <g id="g166" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path168" d="m-8.2,249.2s-0.8-2-5.2-2.4c0,0-22.4-3.6-30.8-16,0,0-6.8-5.6-2.4,6,0,0,10.4,20.4,17.2,23.2,0,0,16.4,4,21.2-10.8z"/> + </g> + <g id="g170" fill="#cc3f4c"> + <path id="path172" d="m71.742,185.23c0.659-7.91,2.612-16.52,0.858-20.03-6.446-12.89-23.419-7.5-34.4,3.2-16,15.6-18-1.2-48,6,0,0-1.745,13.96-0.905,23.98,0,0,37.305-11.58,38.105-5.98,0,0,1.6-3.2,10.8-3.2s31.942-1.17,33.542-3.97z"/> + </g> + <g id="g174" stroke-width="2" stroke="#a51926"> + <path id="path176" d="m28.6,175.2s4.8,4.8,1.2,14.4c0,0-14.4,16-12.4,30"/> + </g> + <g id="g178" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path180" d="m-19.4,260s-4.4-12.8,4.4-6l3.6,3.6c-1.2,1.6-6.8,5.6-8,2.4z"/> + </g> + <g id="g182" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path184" d="m-14.36,261.2s-3.52-10.24,3.52-4.8l2.88,2.88c-4.56,1.28,0,3.84-6.4,1.92z"/> + </g> + <g id="g186" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path188" d="m-9.56,261.2s-3.52-10.24,3.52-4.8l2.88,2.88c-3.36,1.28,0,3.84-6.4,1.92z"/> + </g> + <g id="g190" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path192" d="m-2.96,261.4s-3.52-10.24,3.52-4.8c0,0,4.383,2.33,2.881,2.88-2.961,1.08,0,3.84-6.401,1.92z"/> + </g> + <g id="g194" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path196" d="m3.52,261.32s-3.52-10.24,3.521-4.8l2.88,2.88c-0.96,1.28,0,3.84-6.401,1.92z"/> + </g> + <g id="g198" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path200" d="m10.2,262s-4.8-12.4,4.4-6l3.6,3.6c-1.2,1.6,0,4.8-8,2.4z"/> + </g> + <g id="g202" stroke-width="2" stroke="#a5264c"> + <path id="path204" d="m-18.2,244.8s13.2-2.8,19.2,0.4c0,0,6,1.2,7.2,0.8s4.4-0.8,4.4-0.8"/> + </g> + <g id="g206" stroke-width="2" stroke="#a5264c"> + <path id="path208" d="m15.8,253.6s12-13.6,24-9.2c7.016,2.57,6-0.8,6.8-3.6s1-7,6-10"/> + </g> + <g id="g210" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path212" d="m33,237.6s-4-10.8-6.8,2-6,16.4-7.6,19.2c0,0,0,5.2,8.4,4.8,0,0,10.8-0.4,11.2-3.2s-1.2-14.4-5.2-22.8z"/> + </g> + <g id="g214" stroke-width="2" stroke="#a5264c"> + <path id="path216" d="m47,244.8s3.6-2.4,6-1.2"/> + </g> + <g id="g218" stroke-width="2" stroke="#a5264c"> + <path id="path220" d="m53.5,228.4s2.9-4.9,7.7-5.7"/> + </g> + <g id="g222" fill="#b2b2b2"> + <path id="path224" d="m-25.8,265.2s18,3.2,22.4,1.6l0.4,2-20.8-1.2s-11.6-5.6-2-2.4z"/> + </g> + <g id="g226" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path228" d="m-11.8,172,19.6,0.8s7.2,30.8,3.6,38.4c0,0-1.2,2.8-4-2.8,0,0-18.4-32.8-21.6-34.8s1.2-1.6,2.4-1.6z"/> + </g> + <g id="g230" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path232" d="m-88.9,169.3s8.9,1.7,21.5,4.3c0,0,4.8,22.4,8,27.2s-0.4,4.8-4,2-18.4-16.8-20.4-21.2-5.1-12.3-5.1-12.3z"/> + </g> + <g id="g234" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path236" d="m-67.039,173.82s5.8,1.55,6.809,3.76c1.008,2.22-1.202,5.51-1.202,5.51s-1,3.31-2.202,1.15c-1.202-2.17-4.074-9.83-3.405-10.42z"/> + </g> + <g id="g238" fill="#000"> + <path id="path240" d="m-67,173.6s3.6,5.2,7.2,5.2,3.982-0.41,6.8,0.2c4.6,1,4.2-1,10.8,0.2,2.64,0.48,5.2-0.4,8,0.8s6,0.4,7.2-1.6,6-6.2,6-6.2-12.8,1.8-15.6,2.6c0,0-22.4,1.2-30.4-1.2z"/> + </g> + <g id="g242" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path244" d="m-22.4,173.8s-6.45,3.5-6.85,5.9,5.25,6.1,5.25,6.1,2.75,4.6,3.35,2.2-0.95-13.8-1.75-14.2z"/> + </g> + <g id="g246" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path248" d="m-59.885,179.26s7.007,11.19,7.224-0.02c0,0,0.557-1.26-1.203-1.28-6.075-0.07-4.554-4.18-6.021,1.3z"/> + </g> + <g id="g250" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path252" d="m-52.707,179.51s7.921,11.19,7.285-0.09c0,0,0.007-0.33-1.746-0.48-4.747-0.42-4.402-4.94-5.539,0.57z"/> + </g> + <g id="g254" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path256" d="m-45.494,179.52s7.96,10.63,7.291,0.96c0,0,0.119-1.23-1.535-1.53-3.892-0.71-4.103-3.95-5.756,0.57z"/> + </g> + <g id="g258" stroke-width="0.5" stroke="#000" fill="#FFC"> + <path id="path260" d="m-38.618,179.6s7.9,11.56,8.248,1.78c0,0,1.644-1.38-0.102-1.6-5.818-0.74-5.02-5.19-8.146-0.18z"/> + </g> + <g id="g262" fill="#e5e5b2"> + <path id="path264" d="m-74.792,183.13-7.658-1.53c-2.6-5-4.7-11.15-4.7-11.15s6.35,1,18.85,3.8c0,0,0.876,3.32,2.348,9.11l-8.84-0.23z"/> + </g> + <g id="g266" fill="#e5e5b2"> + <path id="path268" d="m-9.724,178.47c-1.666-2.51-2.983-4.26-3.633-4.67-3.013-1.88,1.13-1.51,2.259-1.51l18.454,0.76s0.524,2.24,1.208,5.63c0,0-10.088-2.01-18.288-0.21z"/> + </g> + <g id="g270" fill="#cc7226"> + <path id="path272" d="m43.88,40.321c27.721,3.96,53.241-31.68,55.001-41.361,1.759-9.68-8.36-21.56-8.36-21.56,1.32-3.08-3.52-17.16-8.8-26.4s-21.181-8.266-38.721-9.24c-15.84-0.88-34.32,22.44-35.64,24.2s4.84,40.041,6.16,45.761-1.32,32.12-1.32,32.12c34.24-9.1,3.96-7.48,31.68-3.52z"/> + </g> + <g id="g274" fill="#ea8e51"> + <path id="path276" d="m8.088-33.392c-1.296,1.728,4.752,39.313,6.048,44.929s-1.296,31.536-1.296,31.536c32.672-8.88,3.888-7.344,31.104-3.456,27.217,3.888,52.273-31.104,54.001-40.609,1.728-9.504-8.208-21.168-8.208-21.168,1.296-3.024-3.456-16.848-8.64-25.92s-20.795-8.115-38.017-9.072c-15.552-0.864-33.696,22.032-34.992,23.76z"/> + </g> + <g id="g278" fill="#efaa7c"> + <path id="path280" d="m8.816-32.744c-1.272,1.696,4.664,38.585,5.936,44.097s-1.272,30.952-1.272,30.952c31.404-9.16,3.816-7.208,30.528-3.392,26.713,3.816,51.305-30.528,53.001-39.857,1.696-9.328-8.056-20.776-8.056-20.776,1.272-2.968-3.392-16.536-8.48-25.44s-20.41-7.965-37.313-8.904c-15.264-0.848-33.072,21.624-34.344,23.32z"/> + </g> + <g id="g282" fill="#f4c6a8"> + <path id="path284" d="m9.544-32.096c-1.248,1.664,4.576,37.857,5.824,43.265s-1.248,30.368-1.248,30.368c29.436-9.04,3.744-7.072,29.952-3.328,26.209,3.744,50.337-29.952,52.001-39.104,1.664-9.153-7.904-20.385-7.904-20.385,1.248-2.912-3.328-16.224-8.32-24.96s-20.025-7.815-36.609-8.736c-14.976-0.832-32.448,21.216-33.696,22.88z"/> + </g> + <g id="g286" fill="#f9e2d3"> + <path id="path288" d="m10.272-31.448c-1.224,1.632,4.488,37.129,5.712,42.433s-1.224,29.784-1.224,29.784c27.868-8.92,3.672-6.936,29.376-3.264,25.705,3.672,49.369-29.376,51.001-38.353,1.632-8.976-7.752-19.992-7.752-19.992,1.224-2.856-3.264-15.912-8.16-24.48s-19.64-7.665-35.905-8.568c-14.688-0.816-31.824,20.808-33.048,22.44z"/> + </g> + <g id="g290" fill="#FFF"> + <path id="path292" d="M44.2,36.8c25.2,3.6,48.401-28.8,50.001-37.6s-7.6-19.6-7.6-19.6c1.2-2.8-3.201-15.6-8.001-24s-19.254-7.514-35.2-8.4c-14.4-0.8-31.2,20.4-32.4,22s4.4,36.4,5.6,41.6-1.2,29.2-1.2,29.2c25.5-8.6,3.6-6.8,28.8-3.2z"/> + </g> + <g id="g294" fill="#CCC"> + <path id="path296" d="m90.601,2.8s-27.801,7.6-39.401,6c0,0-15.8-6.6-24.6,15.2,0,0-3.6,7.2-5.6,9.2s69.601-30.4,69.601-30.4z"/> + </g> + <g id="g298" fill="#000"> + <path id="path300" d="m94.401,0.6s-29.001,12.2-39.001,11.8c0,0-16.4-4.6-24.8,10,0,0-8.4,9.2-11.6,10.8,0,0-0.4,1.6,6-2.4l10.4,5.2s14.8,9.6,24.4-6.4c0,0,4-11.2,4-13.2s21.2-7.6,22.801-8c1.6-0.4,8.2-4.6,7.8-7.8z"/> + </g> + <g id="g302" fill="#99cc32"> + <path id="path304" d="m47,36.514c-6.872,0-15.245-3.865-15.245-10.114,0-6.248,8.373-12.513,15.245-12.513,6.874,0,12.446,5.065,12.446,11.313,0,6.249-5.572,11.314-12.446,11.314z"/> + </g> + <g id="g306" fill="#659900"> + <path id="path308" d="m43.377,19.83c-4.846,0.722-9.935,2.225-9.863,2.009,1.54-4.619,7.901-7.952,13.486-7.952,4.296,0,8.084,1.978,10.32,4.988,0,0-5.316-0.33-13.943,0.955z"/> + </g> + <g id="g310" fill="#FFF"> + <path id="path312" d="m55.4,19.6s-4.4-3.2-4.4-1c0,0,3.6,4.4,4.4,1z"/> + </g> + <g id="g314" fill="#000"> + <path id="path316" d="m45.4,27.726c-2.499,0-4.525-2.026-4.525-4.526,0-2.499,2.026-4.525,4.525-4.525,2.5,0,4.526,2.026,4.526,4.525,0,2.5-2.026,4.526-4.526,4.526z"/> + </g> + <g id="g318" fill="#cc7226"> + <path id="path320" d="m-58.6,14.4s-3.2-21.2-0.8-25.6c0,0,10.8-10,10.4-13.6,0,0-0.4-18-1.6-18.8s-8.8-6.8-14.8-0.4c0,0-10.4,18-9.6,24.4v2s-7.6-0.4-9.2,1.6c0,0-1.2,5.2-2.4,5.6,0,0-2.8,2.4-0.8,5.2,0,0-2,2.4-1.6,6.4l7.6,4s2,14.4,12.8,19.6c4.836,2.329,8-4.4,10-10.4z"/> + </g> + <g id="g322" fill="#FFF"> + <path id="path324" d="m-59.6,12.56s-2.88-19.08-0.72-23.04c0,0,9.72-9,9.36-12.24,0,0-0.36-16.2-1.44-16.92s-7.92-6.12-13.32-0.36c0,0-9.36,16.2-8.64,21.96v1.8s-6.84-0.36-8.28,1.44c0,0-1.08,4.68-2.16,5.04,0,0-2.52,2.16-0.72,4.68,0,0-1.8,2.16-1.44,5.76l6.84,3.6s1.8,12.96,11.52,17.64c4.352,2.095,7.2-3.96,9-9.36z"/> + </g> + <g id="g326" fill="#eb955c"> + <path id="path328" d="m-51.05-42.61c-1.09-0.86-8.58-6.63-14.43-0.39,0,0-10.14,17.55-9.36,23.79v1.95s-7.41-0.39-8.97,1.56c0,0-1.17,5.07-2.34,5.46,0,0-2.73,2.34-0.78,5.07,0,0-1.95,2.34-1.56,6.24l7.41,3.9s1.95,14.04,12.48,19.11c4.714,2.27,7.8-4.29,9.75-10.14,0,0-3.12-20.67-0.78-24.96,0,0,10.53-9.75,10.14-13.26,0,0-0.39-17.55-1.56-18.33z"/> + </g> + <g id="g330" fill="#f2b892"> + <path id="path332" d="m-51.5-41.62c-0.98-0.92-8.36-6.46-14.06-0.38,0,0-9.88,17.1-9.12,23.18v1.9s-7.22-0.38-8.74,1.52c0,0-1.14,4.94-2.28,5.32,0,0-2.66,2.28-0.76,4.94,0,0-1.9,2.28-1.52,6.08l7.22,3.8s1.9,13.68,12.16,18.62c4.594,2.212,7.6-4.18,9.5-9.88,0,0-3.04-20.14-0.76-24.32,0,0,10.26-9.5,9.88-12.92,0,0-0.38-17.1-1.52-17.86z"/> + </g> + <g id="g334" fill="#f8dcc8"> + <path id="path336" d="m-51.95-40.63c-0.87-0.98-8.14-6.29-13.69-0.37,0,0-9.62,16.65-8.88,22.57v1.85s-7.03-0.37-8.51,1.48c0,0-1.11,4.81-2.22,5.18,0,0-2.59,2.22-0.74,4.81,0,0-1.85,2.22-1.48,5.92l7.03,3.7s1.85,13.32,11.84,18.13c4.473,2.154,7.4-4.07,9.25-9.62,0,0-2.96-19.61-0.74-23.68,0,0,9.99-9.25,9.62-12.58,0,0-0.37-16.65-1.48-17.39z"/> + </g> + <g id="g338" fill="#FFF"> + <path id="path340" d="m-59.6,12.46s-2.88-18.98-0.72-22.94c0,0,9.72-9,9.36-12.24,0,0-0.36-16.2-1.44-16.92-0.76-1.04-7.92-6.12-13.32-0.36,0,0-9.36,16.2-8.64,21.96v1.8s-6.84-0.36-8.28,1.44c0,0-1.08,4.68-2.16,5.04,0,0-2.52,2.16-0.72,4.68,0,0-1.8,2.16-1.44,5.76l6.84,3.6s1.8,12.96,11.52,17.64c4.352,2.095,7.2-4.06,9-9.46z"/> + </g> + <g id="g342" fill="#CCC"> + <path id="path344" d="m-62.7,6.2s-21.6-10.2-22.5-11c0,0,9.1,8.2,9.9,8.2s12.6,2.8,12.6,2.8z"/> + </g> + <g id="g346" fill="#000"> + <path id="path348" d="m-79.8,0s18.4,3.6,18.4,8c0,2.912-0.243,16.331-5.6,14.8-8.4-2.4-4.8-16.8-12.8-22.8z"/> + </g> + <g id="g350" fill="#99cc32"> + <path id="path352" d="m-71.4,3.8s8.978,1.474,10,4.2c0.6,1.6,1.263,9.908-4.2,11-4.552,0.911-6.782-9.31-5.8-15.2z"/> + </g> + <g id="g354" fill="#000"> + <path id="path356" d="m14.595,46.349c-0.497-1.742,0.814-1.611,2.605-2.149,2-0.6,14.2-4.4,15-7s14,1.8,14,1.8c1.8,0.8,6.2,3.4,6.2,3.4,4.8,1.2,11.4,1.6,11.4,1.6,2.4,1,5.8,3.8,5.8,3.8,14.6,10.2,27.001,3,27.001,3,19.999-6.6,13.999-23.8,13.999-23.8-3-9,0.2-12.4,0.2-12.4,0.2-3.8,7.4,2.6,7.4,2.6,2.6,4.2,3.4,9.2,3.4,9.2,8,11.2,4.6-6.6,4.6-6.6,0.2-1-2.6-4.6-2.6-5.8s-1.8-4.6-1.8-4.6c-3-3.4-0.6-10.4-0.6-10.4,1.8-13.8-0.4-12-0.4-12-1.2-1.8-10.4,8.2-10.4,8.2-2.2,3.4-8.2,5-8.2,5-2.799,1.8-6.199,0.4-6.199,0.4-2.6-0.4-8.2,6.6-8.2,6.6,2.8-0.2,5.2,4.2,7.6,4.4s4.2-2.4,5.799-3c1.6-0.6,4.4,5.2,4.4,5.2,0.4,2.6-5.2,7.4-5.2,7.4-0.4,4.6-1.999,3-1.999,3-3-0.6-4.2,3.2-5.2,7.8s-5.2,5-5.2,5c-1.6,7.4-2.801,4.4-2.801,4.4-0.2-5.6-6.2,0.2-6.2,0.2-1.2,2-5.8-0.2-5.8-0.2-6.8-2-4.4-4-4.4-4,1.8-2.2,13,0,13,0,2.2-1.6-5.8-5.6-5.8-5.6-0.6-1.8,0.4-6.2,0.4-6.2,1.2-3.2,8-8.8,8-8.8,9.401-1.2,6.601-2.8,6.601-2.8-6.2-5.2-12.001,2.4-12.001,2.4-2.2,6.2-19.6,21.2-19.6,21.2-4.8,3.4-2.2-3.4-6.2,0s-24.6-5.6-24.6-5.6c-11.562-1.193-14.294,14.549-17.823,11.429,0,0,5.418,8.52,3.818,2.92z"/> + </g> + <g id="g358" fill="#000"> + <path id="path360" d="m209.4-120s-25.6,8-28.4,26.8c0,0-2.4,22.8,18,40.4,0,0,0.4,6.4,2.4,9.6,0,0-1.6,4.8,17.2-2.8l27.2-8.4s6.4-2.4,11.6-11.2,20.4-27.6,16.8-52.8c0,0,1.2-11.2-4.8-11.6,0,0-8.4-1.6-15.6,6,0,0-6.8,3.2-9.2,2.8l-35.2,1.2z"/> + </g> + <g id="g362" fill="#000"> + <path id="path364" d="m264.02-120.99s2.1-8.93-2.74-4.09c0,0-7.04,5.72-14.52,5.72,0,0-14.52,2.2-18.92,15.4,0,0-3.96,26.84,3.96,32.56,0,0,4.84,7.48,11.88,0.88s22.54-36.83,20.34-50.47z"/> + </g> + <g id="g366" fill="#323232"> + <path id="path368" d="m263.65-120.63s2.09-8.75-2.66-3.99c0,0-6.92,5.61-14.26,5.61,0,0-14.26,2.16-18.58,15.12,0,0-3.89,26.354,3.89,31.97,0,0,4.75,7.344,11.66,0.864,6.92-6.48,22.11-36.184,19.95-49.574z"/> + </g> + <g id="g370" fill="#666"> + <path id="path372" d="m263.27-120.27s2.08-8.56-2.58-3.9c0,0-6.78,5.51-13.99,5.51,0,0-14,2.12-18.24,14.84,0,0-3.81,25.868,3.82,31.38,0,0,4.66,7.208,11.45,0.848,6.78-6.36,21.66-35.538,19.54-48.678z"/> + </g> + <g id="g374" fill="#999"> + <path id="path376" d="m262.9-119.92s2.07-8.37-2.51-3.79c0,0-6.65,5.41-13.73,5.41,0,0-13.72,2.08-17.88,14.56,0,0-3.75,25.372,3.74,30.78,0,0,4.58,7.072,11.23,0.832,6.66-6.24,21.23-34.892,19.15-47.792z"/> + </g> + <g id="g378" fill="#CCC"> + <path id="path380" d="m262.53-119.56s2.06-8.18-2.43-3.7c0,0-6.53,5.31-13.47,5.31,0,0-13.46,2.04-17.54,14.28,0,0-3.67,24.886,3.67,30.19,0,0,4.49,6.936,11.02,0.816,6.52-6.12,20.79-34.246,18.75-46.896z"/> + </g> + <g id="g382" fill="#FFF"> + <path id="path384" d="m262.15-119.2s2.05-8-2.35-3.6c0,0-6.4,5.2-13.2,5.2,0,0-13.2,2-17.2,14,0,0-3.6,24.4,3.6,29.6,0,0,4.4,6.8,10.8,0.8s20.35-33.6,18.35-46z"/> + </g> + <g id="g386" fill="#992600"> + <path id="path388" d="m50.6,84s-20.4-19.2-28.4-20c0,0-34.4-4-49.2,14,0,0,17.6-20.4,45.2-14.8,0,0-21.6-4.4-34-1.2l-26.4,14-2.8,4.8s4-14.8,22.4-20.8c0,0,22.8-4.8,33.6,0,0,0-21.6-6.8-31.6-4.8,0,0-30.4-2.4-43.2,24,0,0,4-14.4,18.8-21.6,0,0,13.6-8.8,34-6,0,0,14.4,3.2,19.6,5.6s4-0.4-4.4-5.2c0,0-5.6-10-19.6-9.6,0,0-42.8,3.6-53.2,15.6,0,0,13.6-11.2,24-14,0,0,22.4-8,30.8-7.2,0,0,24.8,1,32.4-3,0,0-11.2,5-8,8.2s10,10.8,10,12,24.2,23.3,27.8,27.7l2.2,2.3z"/> + </g> + <g id="g390" fill="#CCC"> + <path id="path392" d="m189,278s-15.5-36.5-28-46c0,0,26,16,29.5,34,0,0,0,10-1.5,12z"/> + </g> + <g id="g394" fill="#CCC"> + <path id="path396" d="m236,285.5s-26.5-55-45-79c0,0,43.5,37.5,48.5,64l0.5,5.5-3-2.5s-0.5,9-1,12z"/> + </g> + <g id="g398" fill="#CCC"> + <path id="path400" d="m292.5,237s-62.5-59.5-64-62c0,0,60.5,66,63.5,73.5,0,0-2-9,0.5-11.5z"/> + </g> + <g id="g402" fill="#CCC"> + <path id="path404" d="m104,280.5s19.5-52,38.5-29.5c0,0,15,10,14.5,13,0,0-4-6.5-22-6,0,0-19-3-31,22.5z"/> + </g> + <g id="g406" fill="#CCC"> + <path id="path408" d="m294.5,153s-45-28.5-52.5-30c-11.81-2.36,49.5,29,54.5,39.5,0,0,2-2.5-2-9.5z"/> + </g> + <g id="g410" fill="#000"> + <path id="path412" d="m143.8,259.6s20.4-2,27.2-8.8l4.4,3.6,17.6-38.4,3.6,5.2s14.4-14.8,13.6-22.8,12.8,6,12.8,6-0.8-11.6,6.4-4.8c0,0-2.4-15.6,6-7.6,0,0-10.54-30.16,12-4.4,5.6,6.4,1.2-0.4,1.2-0.4s-26-48-4.4-33.6c0,0,2-22.8,0.8-27.2s-3.2-26.8-8-32,0.4-6.8,6-1.6c0,0-11.2-24,2-12,0,0-3.6-15.2-8-18,0,0-5.6-17.2,9.6-6.4,0,0-4.4-12.4-7.6-15.6,0,0-11.6-27.6-4.4-22.8l4.4,3.6s-6.8-14-0.4-9.6,6.4,4,6.4,4-21.2-33.2-0.8-15.6c0,0-8.16-13.918-11.6-20.8,0,0-18.8-20.4-4.4-14l4.8,1.6s-8.8-10-16.8-11.6,2.4-8,8.8-6,22,9.6,22,9.6,12.8,18.8,16.8,19.2c0,0-20-7.6-14,0.4,0,0,14.4,14,7.2,13.6,0,0-6,7.2-1.2,16,0,0-18.46-18.391-3.6,7.2l6.8,16.4s-24.4-24.8-13.2-2.8c0,0,17.2,23.6,19.2,24s6.4,9.2,6.4,9.2l-4.4-2,5.2,8.8s-11.2-12-5.2,1.2l5.6,14.4s-20.4-22-6.8,7.6c0,0-16.4-5.2-7.6,12,0,0-1.6,16-1.2,21.2s1.6,33.6-2.8,41.6,6,27.2,8,31.2,5.6,14.8-3.2,5.6-4.4-3.6-2.4,5.2,8,24.4,7.2,30c0,0-1.2,1.2-4.4-2.4,0,0-14.8-22.8-13.2-8.4,0,0-1.2,8-4.4,16.8,0,0-3.2,10.8-3.2,2,0,0-3.2-16.8-6-9.2s-6.4,13.6-9.2,16-8-20.4-9.2-10c0,0-12-12.4-16.8,4l-11.6,16.4s-0.4-12.4-1.6-6.4c0,0-30,6-40.4,1.6z"/> + </g> + <g id="g414" fill="#000"> + <path id="path416" d="m109.4-97.2s-11.599-8-15.599-7.6,27.599-8.8,68.799,18.8c0,0,4.8,2.8,8.4,2.4,0,0,3.2,2.4,0.4,6,0,0-8.8,9.6,2.4,20.8,0,0,18.4,6.8,12.8-2,0,0,10.8,4,13.2,8s1.2,0,1.2,0l-12.4-12.4s-5.2-2-8-10.4-5.2-18.4-0.8-21.6c0,0-4,4.4-3.2,0.4s4.4-7.6,6-8,18-16.2,24.8-16.6c0,0-9.2,1.4-12.2,0.4s-29.6-12.4-35.6-13.6c0,0-16.8-6.6-4.8-4.6,0,0,35.8,3.8,54,17,0,0-7.2-8.4-25.6-15.4,0,0-22.2-12.6-57.4-7.6,0,0-17.8,3.2-25.6,5,0,0-2.599-0.6-3.199-1s-12.401-9.4-40.001-2.4c0,0-17,4.6-25.6,9.4,0,0-15.2,1.2-18.8,4.4,0,0-18.6,14.6-20.6,15.4s-13.4,8.4-14.2,8.8c0,0,24.6-6.6,27-9s19.8-5,22.2-3.6,10.8,0.8,1.2,1.4c0,0,75.6,14.8,76.4,16.8s4.8,0.8,4.8,0.8z"/> + </g> + <g id="g418" fill="#cc7226"> + <path id="path420" d="m180.8-106.4s-10.2-7.4-12.2-7.4-14.4-10.2-18.6-9.8-16.4-9.6-43.8-1.4c0,0-0.6-2,3-2.8,0,0,6.4-2.2,6.8-2.8,0,0,20.2-4.2,27.4-0.6,0,0,9.2,2.6,15.4,8.8,0,0,11.2,3.2,14.4,2.2,0,0,8.8,2.2,9.2,4,0,0,5.8,3,4,5.6,0,0,0.4,1.6-5.6,4.2z"/> + </g> + <g id="g422" fill="#cc7226"> + <path id="path424" d="m168.33-108.51c0.81,0.63,1.83,0.73,2.43,1.54,0.24,0.31-0.05,0.64-0.37,0.74-1.04,0.31-2.1-0.26-3.24,0.33-0.4,0.21-1.04,0.03-1.6-0.12-1.63-0.44-3.46-0.47-5.15,0.22-1.98-1.13-4.34-0.54-6.42-1.55-0.06-0.02-0.28,0.32-0.36,0.3-3.04-1.15-6.79-0.87-9.22-3.15-2.43-0.41-4.78-0.87-7.21-1.55-1.82-0.51-3.23-1.5-4.85-2.33-1.38-0.71-2.83-1.23-4.37-1.61-1.86-0.45-3.69-0.34-5.58-0.86-0.1-0.02-0.29,0.32-0.37,0.3-0.32-0.11-0.62-0.69-0.79-0.64-1.68,0.52-3.17-0.45-4.83-0.11-1.18-1.22-2.9-0.98-4.45-1.42-2.97-0.85-6.12,0.42-9.15-0.58,4.11-1.84,8.8-0.61,12.86-2.68,2.33-1.18,4.99-0.08,7.56-0.84,0.49-0.15,1.18-0.35,1.58,0.32,0.14-0.14,0.32-0.37,0.38-0.35,2.44,1.16,4.76,2.43,7.24,3.5,0.34,0.15,0.88-0.09,1.13,0.12,1.52,1.21,3.46,1.11,4.85,2.33,1.7-0.5,3.49-0.12,5.22-0.75,0.08-0.02,0.31,0.32,0.34,0.3,1.14-0.75,2.29-0.48,3.18-0.18,0.34,0.12,1,0.37,1.31,0.44,1.12,0.27,1.98,0.75,3.16,0.94,0.11,0.02,0.3-0.32,0.37-0.3,1.12,0.44,2.16,0.39,2.82,1.55,0.14-0.14,0.3-0.37,0.38-0.35,1.03,0.34,1.68,1.1,2.78,1.34,0.48,0.1,1.1,0.73,1.67,0.91,2.39,0.73,4.24,2.26,6.43,3.15,0.76,0.31,1.64,0.55,2.27,1.04z"/> + </g> + <g id="g426" fill="#cc7226"> + <path id="path428" d="m91.696-122.74c-2.518-1.72-4.886-2.83-7.328-4.62-0.181-0.13-0.541,0.04-0.743-0.08-1.007-0.61-1.895-1.19-2.877-1.89-0.539-0.38-1.36-0.37-1.868-0.63-2.544-1.29-5.173-1.85-7.68-3.04,0.682-0.64,1.804-0.39,2.4-1.2,0.195,0.28,0.433,0.56,0.786,0.37,1.678-0.9,3.528-1.05,5.204-0.96,1.704,0.09,3.424,0.39,5.199,0.67,0.307,0.04,0.506,0.56,0.829,0.66,2.228,0.66,4.617,0.14,6.736,0.98,1.591,0.63,3.161,1.45,4.4,2.72,0.252,0.26-0.073,0.57-0.353,0.76,0.388-0.11,0.661,0.1,0.772,0.41,0.084,0.24,0.084,0.54,0,0.78-0.112,0.31-0.391,0.41-0.765,0.46-1.407,0.19,0.365-1.19-0.335-0.74-1.273,0.82-0.527,2.22-1.272,3.49-0.28-0.19-0.51-0.41-0.4-0.8,0.234,0.52-0.368,0.81-0.536,1.13-0.385,0.72-1.284,2.14-2.169,1.53z"/> + </g> + <g id="g430" fill="#cc7226"> + <path id="path432" d="m59.198-115.39c-3.154-0.79-6.204-0.68-9.22-1.96-0.067-0.02-0.29,0.32-0.354,0.3-1.366-0.6-2.284-1.56-3.36-2.61-0.913-0.89-2.571-0.5-3.845-0.99-0.324-0.12-0.527-0.63-0.828-0.67-1.219-0.16-2.146-1.11-3.191-1.68,2.336-0.8,4.747-0.76,7.209-1.15,0.113-0.02,0.258,0.31,0.391,0.31,0.136,0,0.266-0.23,0.4-0.36,0.195,0.28,0.497,0.61,0.754,0.35,0.548-0.54,1.104-0.35,1.644-0.31,0.144,0.01,0.269,0.32,0.402,0.32,0.136,0,0.267-0.32,0.4-0.32,0.136,0,0.267,0.32,0.4,0.32,0.136,0,0.266-0.23,0.4-0.36,0.692,0.78,1.577,0.23,2.399,0.41,1.038,0.22,1.305,1.37,2.379,1.67,4.715,1.3,8.852,3.45,13.215,5.54,0.307,0.14,0.517,0.39,0.407,0.78,0.267,0,0.58-0.09,0.77,0.04,1.058,0.74,2.099,1.28,2.796,2.38,0.216,0.34-0.113,0.75-0.346,0.7-4.429-1-8.435-1.61-12.822-2.71z"/> + </g> + <g id="g434" fill="#cc7226"> + <path id="path436" d="m45.338-71.179c-1.592-1.219-2.176-3.25-3.304-5.042-0.214-0.34,0.06-0.654,0.377-0.743,0.56-0.159,1.103,0.319,1.512,0.521,1.745,0.862,3.28,2.104,5.277,2.243,1.99,2.234,6.25,2.619,6.257,6,0.001,0.859-1.427-0.059-1.857,0.8-2.451-1.003-4.84-0.9-7.22-2.367-0.617-0.381-0.287-0.834-1.042-1.412z"/> + </g> + <g id="g438" fill="#cc7226"> + <path id="path440" d="m17.8-123.76c0.135,0,7.166,0.24,7.149,0.35-0.045,0.31-7.775,1.36-8.139,1.19-0.164-0.08-7.676,2.35-7.81,2.22,0.268-0.14,8.534-3.76,8.8-3.76z"/> + </g> + <g id="g442" fill="#000"> + <path id="path444" d="m33.2-114s-14.8,1.8-19.2,3-23,8.8-26,10.8c0,0-13.4,5.4-30.4,25.4,0,0,7.6-3.4,9.8-6.2,0,0,13.6-12.6,13.4-10,0,0,12.2-8.6,11.6-6.4,0,0,24.4-11.2,22.4-8,0,0,21.6-4.6,20.6-2.6,0,0,18.8,4.4,16,4.6,0,0-5.8,1.2,0.6,4.8,0,0-3.4,4.4-8.8,0.4s-2.4-1.8-7.4-0.8c0,0-2.6,0.8-7.2-3.2,0,0-5.6-4.6-14.4-1,0,0-30.6,12.6-32.6,13.2,0,0-3.6,2.8-6,6.4,0,0-5.8,4.4-8.8,5.8,0,0-12.8,11.6-14,13,0,0-3.4,5.2-4.2,5.6,0,0,6.4-3.8,8.4-5.8,0,0,14-10,19.4-10.8,0,0,4.4-3,5.2-4.4,0,0,14.4-9.2,18.6-9.2,0,0,9.2,5.2,11.6-1.8,0,0,5.8-1.8,11.4-0.6,0,0,3.2-2.6,2.4-4.8,0,0,1.6-1.8,2.6,2,0,0,3.4,3.6,8.2,1.6,0,0,4-0.2,2,2.2,0,0-4.4,3.8-16.2,4,0,0-12.4,0.6-28.8,8.2,0,0-29.8,10.4-39,20.8,0,0-6.4,8.8-11.8,10,0,0-5.8,0.8-11.8,8.2,0,0,9.8-5.8,18.8-5.8,0,0,4-2.4,0.2,1.2,0,0-3.6,7.6-2,13,0,0-0.6,5.2-1.4,6.8,0,0-7.8,12.8-7.8,15.2s1.2,12.2,1.6,12.8-1-1.6,2.8,0.8,6.6,4,7.4,6.8-2-5.4-2.2-7.2-4.4-9-3.6-11.4c0,0,1,1,1.8,2.4,0,0-0.6-0.6,0-4.2,0,0,0.8-5.2,2.2-8.4s3.4-7,3.8-7.8,0.4-6.6,1.8-4l3.4,2.6s-2.8-2.6-0.6-4.8c0,0-1-5.6,0.8-8.2,0,0,7-8.4,8.6-9.4s0.2-0.6,0.2-0.6,6-4.2,0.2-2.6c0,0-4,1.6-7,1.6,0,0-7.6,2-3.6-2.2s14-9.6,17.8-9.4l0.8,1.6,11.2-2.4-1.2,0.8s-0.2-0.2,4-0.6,10,1,11.4-0.8,4.8-2.8,4.4-1.4-0.6,3.4-0.6,3.4,5-5.8,4.4-3.6-8.8,7.4-10.2,13.6l10.4-8.2,3.6-3s3.6,2.2,3.8,0.6,4.8-7.4,6-7.2,3.2-2.6,3,0,7.4,8,7.4,8,3.2-1.8,4.6-0.4,5.6-19.8,5.6-19.8l25-10.6,43.6-3.4-16.999-6.8-61.001-11.4z"/> + </g> + <g id="g446" stroke-width="2" stroke="#4c0000"> + <path id="path448" d="m51.4,85s-15-16.8-23.4-19.4c0,0-13.4-6.8-38,1"/> + </g> + <g id="g450" stroke-width="2" stroke="#4c0000"> + <path id="path452" d="m24.8,64.2s-25.2-8-40.6-3.8c0,0-18.4,2-26.8,15.8"/> + </g> + <g id="g454" stroke-width="2" stroke="#4c0000"> + <path id="path456" d="m21.2,63s-17-7.2-31.8-9.4c0,0-16.6-2.6-33.2,4.6,0,0-12.2,6-17.6,16.2"/> + </g> + <g id="g458" stroke-width="2" stroke="#4c0000"> + <path id="path460" d="m22.2,63.4s-15.4-11-16.4-12.4c0,0-7-11-20-11.4,0,0-21.4,0.8-38.6,8.8"/> + </g> + <g id="g462" fill="#000"> + <path id="path464" d="M20.895,54.407c1.542,1.463,28.505,30.393,28.505,30.393,35.2,36.6,7.2,2.4,7.2,2.4-7.6-4.8-16.8-23.6-16.8-23.6-1.2-2.8,14,7.2,14,7.2,4,0.8,17.6,20,17.6,20-6.8-2.4-2,4.8-2,4.8,2.8,2,23.201,17.6,23.201,17.6,3.6,4,7.599,5.6,7.599,5.6,14-5.2,7.6,8,7.6,8,2.4,6.8,8-4.8,8-4.8,11.2-16.8-5.2-14.4-5.2-14.4-30,2.8-36.8-13.2-36.8-13.2-2.4-2.4,6.4,0,6.4,0,8.401,2-7.2-12.4-7.2-12.4,2.4,0,11.6,6.8,11.6,6.8,10.401,9.2,12.401,7.2,12.401,7.2,17.999-8.8,28.399-1.2,28.399-1.2,2,1.6-3.6,8.4-2,13.6s6.4,17.6,6.4,17.6c-2.4,1.6-2,12.4-2,12.4,16.8,23.2,7.2,21.2,7.2,21.2-15.6-0.4-0.8,7.2-0.8,7.2,3.2,2,12,9.2,12,9.2-2.8-1.2-4.4,4-4.4,4,4.8,4,2,8.8,2,8.8-6,1.2-7.2,5.2-7.2,5.2,6.8,8-3.2,8.4-3.2,8.4,3.6,4.4-1.2,16.4-1.2,16.4-4.8,0-11.2,5.6-11.2,5.6,2.4,4.8-8,10.4-8,10.4-8.4,1.6-5.6,8.4-5.6,8.4-7.999,6-10.399,22-10.399,22-0.8,10.4-3.2,13.6,2,11.6,5.199-2,4.399-14.4,4.399-14.4-4.799-15.6,38-31.6,38-31.6,4-1.6,4.8-6.8,4.8-6.8,2,0.4,10.8,8,10.8,8,7.6,11.2,8,2,8,2,1.2-3.6-0.4-9.6-0.4-9.6,6-21.6-8-28-8-28-10-33.6,4-25.2,4-25.2,2.8,5.6,13.6,10.8,13.6,10.8l3.6-2.4c-1.6-4.8,6.8-10.8,6.8-10.8,2.8,6.4,8.8-1.6,8.8-1.6,3.6-24.4,16-10,16-10,4,1.2,5.2-5.6,5.2-5.6,3.6-10.4,0-24,0-24,3.6-0.4,13.2,5.6,13.2,5.6,2.8-3.6-6.4-20.4-2.4-18s8.4,4,8.4,4c0.8-2-9.2-14.4-9.2-14.4-4.4-2.8-9.6-23.2-9.6-23.2,7.2,3.6-2.8-11.6-2.8-11.6,0-3.2,6-14.4,6-14.4-0.8-6.8,0-6.4,0-6.4,2.8,1.2,10.8,2.8,4-3.6s0.8-11.2,0.8-11.2c4.4-2.8-9.2-2.4-9.2-2.4-5.2-4.4-4.8-8.4-4.8-8.4,8,2-6.4-12.4-8.8-16s7.2-8.8,7.2-8.8c13.2-3.6,1.6-6.8,1.6-6.8-19.6,0.4-8.8-10.4-8.8-10.4,6,0.4,4.4-2,4.4-2-5.2-1.2-14.8-7.6-14.8-7.6-4-3.6-0.4-2.8-0.4-2.8,16.8,1.2-12-10-12-10,8,0-10-10.4-10-10.4-2-1.6-5.2-9.2-5.2-9.2-6-5.2-10.8-12-10.8-12-0.4-4.4-5.2-9.2-5.2-9.2-11.6-13.6-17.2-13.2-17.2-13.2-14.8-3.6-20-2.8-20-2.8l-52.8,4.4c-26.4,12.8-18.6,33.8-18.6,33.8,6.4,8.4,15.6,4.6,15.6,4.6,4.6-6.2,16.2-4,16.2-4,20.401,3.2,17.801-0.4,17.801-0.4-2.4-4.6-18.601-10.8-18.801-11.4s-9-4-9-4c-3-1.2-7.4-10.4-7.4-10.4-3.2-3.4,12.6,2.4,12.6,2.4-1.2,1,6.2,5,6.2,5,17.401-1,28.001,9.8,28.001,9.8,10.799,16.6,10.999,8.4,10.999,8.4,2.8-9.4-9-30.6-9-30.6,0.4-2,8.6,4.6,8.6,4.6,1.4-2,2.2,3.8,2.2,3.8,0.2,2.4,4,10.4,4,10.4,2.8,13,6.4,5.6,6.4,5.6l4.6,9.4c1.4,2.6-4.6,10.2-4.6,10.2-0.2,2.8,0.6,2.6-5,10.2s-2.2,12-2.2,12c-1.4,6.6,7.4,6.2,7.4,6.2,2.6,2.2,6,2.2,6,2.2,1.8,2,4.2,1.4,4.2,1.4,1.6-3.8,7.8-1.8,7.8-1.8,1.4-2.4,9.6-2.8,9.6-2.8,1-2.6,1.4-4.2,4.8-4.8s-21.2-43.6-21.2-43.6c6.4-0.8-1.8-13.2-1.8-13.2-2.2-6.6,9.2,8,11.4,9.4s3.2,3.6,1.6,3.4-3.4,2-2,2.2,14.4,15.2,17.8,25.4,9.4,14.2,15.6,20.2,5.4,30.2,5.4,30.2c-0.4,8.8,5.6,19.4,5.6,19.4,2,3.8-2.2,22-2.2,22-2,2.2-0.6,3-0.6,3,1,1.2,7.8,14.4,7.8,14.4-1.8-0.2,1.8,3.4,1.8,3.4,5.2,6-1.2,3-1.2,3-6-1.6,1,8.2,1,8.2,1.2,1.8-7.8-2.8-7.8-2.8-9.2-0.6,2.4,6.6,2.4,6.6,8.6,7.2-2.8,2.8-2.8,2.8-4.6-1.8-1.4,5-1.4,5,3.2,1.6,20.4,8.6,20.4,8.6,0.4,3.8-2.6,8.8-2.6,8.8,0.4,4-1.8,7.4-1.8,7.4-1.2,8.2-1.8,9-1.8,9-4.2,0.2-11.6,14-11.6,14-1.8,2.6-12,14.6-12,14.6-2,7-20-0.2-20-0.2-6.6,3.4-4.6,0-4.6,0-0.4-2.2,4.4-8.2,4.4-8.2,7-2.6,4.4-13.4,4.4-13.4,4-1.4-7.2-4.2-7-5.4s6-2.6,6-2.6c8-2,3.6-4.4,3.6-4.4-0.6-4,2.4-9.6,2.4-9.6,11.6-0.8,0-17,0-17-10.8-7.6-11.8-13.4-11.8-13.4,12.6-8.2,4.4-20.6,4.6-24.2s1.4-25.2,1.4-25.2c-2-6.2-5-19.8-5-19.8,2.2-5.2,9.6-17.8,9.6-17.8,2.8-4.2,11.6-9,9.4-12s-10-1.2-10-1.2c-7.8-1.4-7.2,3.8-7.2,3.8-1.6,1-2.4,6-2.4,6-0.72,7.933-9.6,14.2-9.6,14.2-11.2,6.2-2,10.2-2,10.2,6,6.6-3.8,6.8-3.8,6.8-11-1.8-2.8,8.4-2.8,8.4,10.8,12.8,7.8,15.6,7.8,15.6-10.2,1,2.4,10.2,2.4,10.2s-0.8-2-0.6-0.2,3.2,6,4,8-3.2,2.2-3.2,2.2c0.6,9.6-14.8,5.4-14.8,5.4l-1.6,0.2c-1.6,0.2-12.8-0.6-18.6-2.8s-12.599-2.2-12.599-2.2-4,1.8-11.601,1.6c-7.6-0.2-15.6,2.6-15.6,2.6-4.4-0.4,4.2-4.8,4.4-4.6s5.8-5.4-2.2-4.8c-21.797,1.635-32.6-8.6-32.6-8.6-2-1.4-4.6-4.2-4.6-4.2-10-2,1.4,12.4,1.4,12.4,1.2,1.4-0.2,2.4-0.2,2.4-0.8-1.6-8.6-7-8.6-7-2.811-0.973-4.174-2.307-6.505-4.793z"/> + </g> + <g id="g466" fill="#4c0000"> + <path id="path468" d="m-3,42.8s11.6,5.6,14.2,8.4,16.6,14.2,16.6,14.2-5.4-2-8-3.8-13.4-10-13.4-10-3.8-6-9.4-8.8z"/> + </g> + <g id="g470" fill="#99cc32"> + <path id="path472" d="M-61.009,11.603c0.337-0.148-0.187-2.86-0.391-3.403-1.022-2.726-10-4.2-10-4.2-0.227,1.365-0.282,2.961-0.176,4.599,0,0,4.868,5.519,10.567,3.004z"/> + </g> + <g id="g474" fill="#659900"> + <path id="path476" d="M-61.009,11.403c-0.449,0.158-0.015-2.734-0.191-3.203-1.022-2.726-10.2-4.3-10.2-4.3-0.227,1.365-0.282,2.961-0.176,4.599,0,0,4.268,5.119,10.567,2.904z"/> + </g> + <g id="g478" fill="#000"> + <path id="path480" d="m-65.4,11.546c-0.625,0-1.131-1.14-1.131-2.546,0-1.405,0.506-2.545,1.131-2.545s1.132,1.14,1.132,2.545c0,1.406-0.507,2.546-1.132,2.546z"/> + </g> + <g id="g482" fill="#000"> + <path id="path484" d="M-65.4,9z"/> + </g> + <g id="g486" fill="#000"> + <path id="path488" d="m-111,109.6s-5.6,10,19.2,4c0,0,14-1.2,16.4-3.6,1.2,0.8,9.566,3.73,12.4,4.4,6.8,1.6,15.2-8.4,15.2-8.4s4.6-10.5,7.4-10.5-0.4,1.6-0.4,1.6-6.6,10.1-6.2,11.7c0,0-5.2,20-21.2,20.8,0,0-16.15,0.95-14.8,6.8,0,0,8.8-2.4,11.2,0,0,0,10.8-0.4,2.8,6l-6.8,11.6s0.14,3.92-10,0.4c-9.8-3.4-20.1-16.3-20.1-16.3s-15.95-14.55-5.1-28.5z"/> + </g> + <g id="g490" fill="#e59999"> + <path id="path492" d="m-112.2,113.6s-2,9.6,34.8-0.8l6.8,0.8c2.4,0.8,14.4,3.6,16.4,2.4,0,0-7.2,13.6-18.8,12,0,0-13.2,1.6-12.8,6.4,0,0,4,7.2,8.8,9.6,0,0,2.8,2.4,2.4,5.6s-3.2,4.8-5.2,5.6-5.2-2.4-6.8-2.4-10-6.4-14.4-11.2-12.8-16.8-12.4-19.6,1.2-8.4,1.2-8.4z"/> + </g> + <g id="g494" fill="#b26565"> + <path id="path496" d="m-109,131.05c2.6,3.95,5.8,8.15,8,10.55,4.4,4.8,12.8,11.2,14.4,11.2s4.8,3.2,6.8,2.4,4.8-2.4,5.2-5.6-2.4-5.6-2.4-5.6c-3.066-1.53-5.806-5.02-7.385-7.35,0,0,0.185,2.55-5.015,1.75s-10.4-3.6-12-6.8-4-5.6-2.4-2,4,7.2,5.6,7.6,1.2,1.6-1.2,1.2-5.2-0.8-9.6-6z"/> + </g> + <g id="g498" fill="#992600"> + <path id="path500" d="m-111.6,110s1.8-13.6,3-17.6c0,0-0.8-6.8,1.6-11s4.4-10.4,7.4-15.8,3.2-9.4,7.2-11,10-10.2,12.8-11.2,2.6-0.2,2.6-0.2,6.8-14.8,20.4-10.8c0,0-16.2-2.8-0.4-12.2,0,0-4.8,1.1-1.5-5.9,2.201-4.668,1.7,2.1-9.3,13.9,0,0-5,8.6-10.2,11.6s-17.2,10-18.4,13.8-4.4,9.6-6.4,11.2-4.8,5.8-5.2,9.2c0,0-1.2,4-2.6,5.2s-1.6,4.4-1.6,6.4-2,4.8-1.8,7.2c0,0,0.8,19,0.4,21l2-3.8z"/> + </g> + <g id="g502" fill="#FFF"> + <path id="path504" d="m-120.2,114.6s-2-1.4-6.4,4.6c0,0,7.3,33,7.3,34.4,0,0,1.1-2.1-0.2-9.3s-2.2-19.9-2.2-19.9l1.5-9.8z"/> + </g> + <g id="g506" fill="#992600"> + <path id="path508" d="m-98.6,54s-17.6,3.2-17.2,32.4l-0.8,24.8s-1.2-25.6-2.4-27.2,2.8-12.8-0.4-6.8c0,0-14,14-6,35.2,0,0,1.5,3.3-1.5-1.3,0,0-4.6-12.6-3.5-19,0,0,0.2-2.2,2.1-5,0,0,8.6-11.7,11.3-14,0,0,1.8-14.4,17.2-19.6,0,0,5.7-2.3,1.2,0.5z"/> + </g> + <g id="g510" fill="#000"> + <path id="path512" d="m40.8-12.2c0.66-0.354,0.651-1.324,1.231-1.497,1.149-0.344,1.313-1.411,1.831-2.195,0.873-1.319,1.066-2.852,1.648-4.343,0.272-0.7,0.299-1.655-0.014-2.315-1.174-2.481-1.876-4.93-3.318-7.356-0.268-0.45-0.53-1.244-0.731-1.842-0.463-1.384-1.72-2.375-2.58-3.695-0.288-0.441,0.237-1.366-0.479-1.45-0.897-0.105-2.346-0.685-2.579,0.341-0.588,2.587,0.423,5.11,1.391,7.552-0.782,0.692-0.448,1.613-0.296,2.38,0.71,3.606-0.488,6.958-1.249,10.432-0.023,0.104,0.319,0.302,0.291,0.364-1.222,2.686-2.674,5.131-4.493,7.512-0.758,0.992-1.63,1.908-2.127,2.971-0.368,0.787-0.776,1.753-0.526,2.741-3.435,2.78-5.685,6.625-8.296,10.471-0.462,0.68-0.171,1.889,0.38,2.158,0.813,0.398,1.769-0.626,2.239-1.472,0.389-0.698,0.742-1.348,1.233-1.991,0.133-0.175-0.046-0.594,0.089-0.715,2.633-2.347,4.302-5.283,6.755-7.651,1.95-0.329,3.487-1.327,5.235-2.34,0.308-0.179,0.832,0.07,1.122-0.125,1.753-1.177,1.751-3.213,1.857-5.123,0.05-0.884,0.246-2.201,1.386-2.812z"/> + </g> + <g id="g514" fill="#000"> + <path id="path516" d="m31.959-16.666c0.124-0.077-0.031-0.5,0.078-0.716,0.162-0.324,0.565-0.512,0.727-0.836,0.109-0.216-0.054-0.596,0.082-0.738,2.333-2.447,2.59-5.471,1.554-8.444,1.024-0.62,1.085-1.882,0.66-2.729-0.853-1.7-1.046-3.626-2.021-5.169-0.802-1.269-2.38-2.513-3.751-1.21-0.421,0.4-0.742,1.187-0.464,1.899,0.064,0.163,0.349,0.309,0.322,0.391-0.107,0.324-0.653,0.548-0.659,0.82-0.03,1.496-0.984,3.007-0.354,4.336,0.772,1.629,1.591,3.486,2.267,5.262-1.234,2.116-0.201,4.565-1.954,6.442-0.136,0.146-0.127,0.532-0.005,0.734,0.292,0.486,0.698,0.892,1.184,1.184,0.202,0.121,0.55,0.123,0.75-0.001,0.578-0.362,0.976-0.849,1.584-1.225z"/> + </g> + <g id="g518" fill="#000"> + <path id="path520" d="m94.771-26.977c1.389,1.792,1.679,4.587-0.37,5.977,0.55,3.309,3.901,1.33,5.999,0.8-0.11-0.388,0.12-0.732,0.4-0.737,1.06-0.015,1.74-1.047,2.8-0.863,0.44-1.557,2.07-2.259,2.72-3.639,1.72-3.695,1.13-7.968-1.45-11.214-0.2-0.254,0.01-0.771-0.11-1.133-0.76-2.211-2.82-2.526-4.76-3.214-1.176-3.875-1.837-7.906-3.599-11.6-1.614-0.25-2.312-1.989-3.649-2.709-1.333-0.719-1.901,0.86-1.86,1.906,0.007,0.205,0.459,0.429,0.289,0.794-0.076,0.164-0.336,0.275-0.336,0.409,0.001,0.135,0.222,0.266,0.356,0.4-0.918,0.82-2.341,1.297-2.636,2.442-0.954,3.71,1.619,6.835,3.287,10.036,0.591,1.135-0.145,2.406-0.905,3.614-0.438,0.695-0.33,1.822-0.054,2.678,0.752,2.331,2.343,4.07,3.878,6.053z"/> + </g> + <g id="g522" fill="#000"> + <path id="path524" d="m57.611-8.591c-1.487,1.851-4.899,4.42-1.982,6.348,0.194,0.129,0.564,0.133,0.737-0.001,2.021-1.565,4.024-2.468,6.46-3.05,0.124-0.029,0.398,0.438,0.767,0.277,1.613-0.703,3.623-0.645,4.807-1.983,3.767,0.224,7.332-0.892,10.723-2.2,1.161-0.448,2.431-1.007,3.632-1.509,1.376-0.576,2.58-1.504,3.692-2.645,0.133-0.136,0.487-0.046,0.754-0.046-0.04-0.863,0.922-0.99,1.169-1.612,0.092-0.232-0.058-0.628,0.075-0.73,2.138-1.63,3.058-3.648,1.889-6.025-0.285-0.578-0.534-1.196-1.1-1.672-1.085-0.911-2.187-0.057-3.234-0.361-0.159,0.628-0.888,0.456-1.274,0.654-0.859,0.439-2.192-0.146-3.051,0.292-1.362,0.695-2.603,0.864-4.025,1.241-0.312,0.082-1.09-0.014-1.25,0.613-0.134-0.134-0.282-0.368-0.388-0.346-1.908,0.396-3.168,0.61-4.469,2.302-0.103,0.133-0.545-0.046-0.704,0.089-0.957,0.808-1.362,2.042-2.463,2.714-0.201,0.123-0.553-0.045-0.747,0.084-0.646,0.431-1.013,1.072-1.655,1.519-0.329,0.229-0.729-0.096-0.697-0.352,0.245-1.947,0.898-3.734,0.323-5.61,2.077-2.52,4.594-4.469,6.4-7.2,0.015-2.166,0.707-4.312,0.594-6.389-0.01-0.193-0.298-0.926-0.424-1.273-0.312-0.854,0.594-1.92-0.25-2.644-1.404-1.203-2.696-0.327-3.52,1.106-1.838,0.39-3.904,1.083-5.482-0.151-1.007-0.787-1.585-1.693-2.384-2.749-0.985-1.302-0.65-2.738-0.58-4.302,0.006-0.128-0.309-0.264-0.309-0.398,0.001-0.135,0.221-0.266,0.355-0.4-0.706-0.626-0.981-1.684-2-2,0.305-1.092-0.371-1.976-1.242-2.278-1.995-0.691-3.672,1.221-5.564,1.294-0.514,0.019-0.981-1.019-1.63-1.344-0.432-0.216-1.136-0.249-1.498,0.017-0.688,0.504-1.277,0.618-2.035,0.823-1.617,0.436-2.895,1.53-4.375,2.385-1.485,0.857-2.44,2.294-3.52,3.614-0.941,1.152-1.077,3.566,0.343,4.066,1.843,0.65,3.147-2.053,5.113-1.727,0.312,0.051,0.518,0.362,0.408,0.75,0.389,0.109,0.607-0.12,0.8-0.4,0.858,1.019,2.022,1.356,2.96,2.229,0.97,0.904,2.716,0.486,3.731,1.483,1.529,1.502,0.97,4.183,2.909,5.488-0.586,1.313-1.193,2.59-1.528,4.017-0.282,1.206,0.712,2.403,1.923,2.312,1.258-0.094,1.52-0.853,2.005-1.929,0.267,0.267,0.736,0.564,0.695,0.78-0.457,2.387-1.484,4.38-1.942,6.811-0.059,0.317-0.364,0.519-0.753,0.409-0.468,4.149-4.52,6.543-7.065,9.708-0.403,0.502-0.407,1.751,0.002,2.154,1.403,1.387,3.363-0.159,5.063-0.662,0.213-1.206,1.072-2.148,2.404-2.092,0.256,0.01,0.491-0.532,0.815-0.662,0.348-0.138,0.85,0.086,1.136-0.112,1.729-1.195,3.137-2.301,4.875-3.49,0.192-0.131,0.536,0.028,0.752-0.08,0.325-0.162,0.512-0.549,0.835-0.734,0.348-0.2,0.59,0.09,0.783,0.37-0.646,0.349-0.65,1.306-1.232,1.508-0.775,0.268-1.336,0.781-2.01,1.228-0.292,0.193-0.951-0.055-1.055,0.124-0.598,1.028-1.782,1.466-2.492,2.349z"/> + </g> + <g id="g526" fill="#000"> + <path id="path528" d="m2.2-58s-9.238-2.872-20.4,22.8c0,0-2.4,5.2-4.8,7.2s-13.6,5.6-15.6,9.6l-10.4,16s14.8-16,18-18.4c0,0,8-8.4,4.8-1.6,0,0-14,10.8-12.8,20,0,0-5.6,14.4-6.4,16.4,0,0,16-32,18.4-33.2s3.6-1.2,2.4,2.4-1.6,20-4.4,22c0,0,8-20.4,7.2-23.6,0,0,3.2-3.6,5.6,1.6l-1.2,16,4.4,12s-2.4-11.2-0.8-26.8c0,0-2-10.4,2-4.8s13.6,11.6,13.6,16.4c0,0-5.2-17.6-14.4-22.4l-4,6-1.2-2s-3.6-0.8,0.8-7.6,4-7.6,4-7.6,6.4,7.2,8,7.2c0,0,13.2-7.6,14.4,16.8,0,0,6.8-14.4-2.4-21.2,0,0-14.8-2-13.6-7.2l7.2-12.4c3.6-5.2,2-2.4,2-2.4z"/> + </g> + <g id="g530" fill="#000"> + <path id="path532" d="m-17.8-41.6-16,5.2-7.2,9.6s17.2-10,21.2-11.2,2-3.6,2-3.6z"/> + </g> + <g id="g534" fill="#000"> + <path id="path536" d="m-57.8-35.2s-2,1.2-2.4,4-2.8,3.2-2,6,2.8,5.2,2.8,1.2,1.6-6,2.4-7.2,2.4-5.6-0.8-4z"/> + </g> + <g id="g538" fill="#000"> + <path id="path540" d="m-66.6,26s-8.4-4-11.6-7.6-2.748,1.566-7.6,1.2c-5.847-0.441-4.8-16.4-4.8-16.4l-4,7.6s-1.2,14.4,6.8,12c3.907-1.172,5.2,0.4,3.6,1.2s5.6,1.2,2.8,2.8,11.6-3.6,9.2,6.8l5.6-7.6z"/> + </g> + <g id="g542" fill="#000"> + <path id="path544" d="m-79.2,40.4s-15.4,4.4-19-5.2c0,0-4.8,2.4-2.6,5.4s3.4,3.4,3.4,3.4,5.4,1.2,4.8,2-3,4.2-3,4.2,10.2-6,16.4-9.8z"/> + </g> + <g id="g546" fill="#FFF"> + <path id="path548" d="m149.2,118.6c-0.43,2.14-2.1,2.94-4,3.6-1.92-0.96-4.51-4.06-6.4-2-0.47-0.48-1.25-0.54-1.6-1.2-0.46-0.9-0.19-1.94-0.53-2.74-0.55-1.28-1.25-2.64-1.07-4.06,1.81-0.71,2.4-2.62,1.93-4.38-0.07-0.26-0.5-0.45-0.3-0.8,0.19-0.33,0.5-0.55,0.77-0.82-0.13,0.14-0.28,0.37-0.39,0.35-0.61-0.11-0.49-0.75-0.36-1.13,0.59-1.75,2.6-2.01,3.95-0.82,0.26-0.56,0.77-0.37,1.2-0.4-0.05-0.58,0.36-1.11,0.56-1.53,0.52-1.09,2.14,0.01,2.94-0.6,1.08-0.83,2.14-1.52,3.22-0.92,1.81,1.01,3.52,2.22,4.72,3.97,0.57,0.83,0.81,2.11,0.75,3.07-0.04,0.65-1.42,0.29-1.76,1.22-0.65,1.75,1.19,2.27,1.94,3.61,0.2,0.35-0.06,0.65-0.38,0.75-0.41,0.13-1.19-0.06-1.06,0.39,0.98,3.19-1.78,3.87-4.13,4.44z"/> + </g> + <g id="g550" fill="#FFF"> + <path id="path552" d="m139.6,138.2c-0.01-1.74-1.61-3.49-0.4-5.2,0.14,0.14,0.27,0.36,0.4,0.36,0.14,0,0.27-0.22,0.4-0.36,1.5,2.22,5.15,3.14,5.01,5.99-0.03,0.45-1.11,1.37-0.21,2.01-1.81,1.35-1.87,3.72-2.8,5.6-1.24-0.28-2.45-0.65-3.6-1.2,0.35-1.48,0.24-3.17,1.06-4.49,0.43-0.7,0.14-1.78,0.14-2.71z"/> + </g> + <g id="g554" fill="#CCC"> + <path id="path556" d="m-26.6,129.2s-16.858,10.14-2.8-5.2c8.8-9.6,18.8-15.2,18.8-15.2s10.4-4.4,14-5.6,18.8-6.4,22-6.8,12.8-4.4,19.6-0.4,14.8,8.4,14.8,8.4-16.4-8.4-20-6-10.8,2-16.8,5.2c0,0-14.8,4.4-18,6.4s-13.6,13.6-15.2,12.8,0.4-1.2,1.6-4-0.8-4.4-8.8,2-9.2,8.4-9.2,8.4z"/> + </g> + <g id="g558" fill="#000"> + <path id="path560" d="m-19.195,123.23s1.41-13.04,9.888-11.37c0,0,8.226-4.17,10.948-6.14,0,0,8.139-1.7,9.449-2.32,18.479-8.698,33.198-4.179,33.745-5.299,0.546-1.119,20.171,5.999,23.78,10.079,0.391,0.45-10.231-5.59-19.929-7.48-8.273-1.617-29.875,0.24-40.781,5.78-2.973,1.51-11.918,7.29-14.449,7.18s-12.651,9.57-12.651,9.57z"/> + </g> + <g id="g562" fill="#CCC"> + <path id="path564" d="m-23,148.8s-15.2-2.4,1.6-4c0,0,18-2,22-7.2,0,0,13.6-9.2,16.4-9.6s32.8-7.6,33.2-10,6-2.4,7.6-1.6,0.8,2-2,2.8-34,17.2-40.4,18.4-18,8.8-22.8,10-15.6,1.2-15.6,1.2z"/> + </g> + <g id="g566" fill="#000"> + <path id="path568" d="m-3.48,141.4s-8.582-0.83,0.019-1.64c0,0,8.816-3.43,10.864-6.09,0,0,6.964-4.71,8.397-4.92,1.434-0.2,15.394-3.89,15.599-5.12s34.271-13.81,38.691-10.62c2.911,2.1-6.99,0.43-16.624,4.84-1.355,0.62-35.208,15.2-38.485,15.82-3.277,0.61-9.216,4.5-11.674,5.12-2.457,0.61-6.787,2.61-6.787,2.61z"/> + </g> + <g id="g570" fill="#000"> + <path id="path572" d="m-11.4,143.6s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g574" fill="#000"> + <path id="path576" d="m-18.6,145.2s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g578" fill="#000"> + <path id="path580" d="m-29,146.8s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g582" fill="#000"> + <path id="path584" d="m-36.6,147.6s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g586" fill="#000"> + <path id="path588" d="m1.8,108,3.2,1.6c-1.2,1.6-4.4,1.2-4.4,1.2l1.2-2.8z"/> + </g> + <g id="g590" fill="#000"> + <path id="path592" d="m-8.2,113.6s6.506-2.14,4,1.2c-1.2,1.6-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g594" fill="#000"> + <path id="path596" d="m-19.4,118.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g598" fill="#000"> + <path id="path600" d="m-27,124.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g602" fill="#000"> + <path id="path604" d="m-33.8,129.2s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g606" fill="#000"> + <path id="path608" d="m5.282,135.6s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/> + </g> + <g id="g610" fill="#000"> + <path id="path612" d="m15.682,130.8s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/> + </g> + <g id="g614" fill="#000"> + <path id="path616" d="m26.482,126.4s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/> + </g> + <g id="g618" fill="#000"> + <path id="path620" d="m36.882,121.6s6.921-0.53,5.324,1.6c-1.597,2.12-4.792,1.06-4.792,1.06l-0.532-2.66z"/> + </g> + <g id="g622" fill="#000"> + <path id="path624" d="m9.282,103.6s6.921-0.53,5.324,1.6c-1.597,2.12-5.592,1.86-5.592,1.86l0.268-3.46z"/> + </g> + <g id="g626" fill="#000"> + <path id="path628" d="m19.282,100.4s6.921-0.534,5.324,1.6c-1.597,2.12-5.992,1.86-5.992,1.86l0.668-3.46z"/> + </g> + <g id="g630" fill="#000"> + <path id="path632" d="m-3.4,140.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g634" fill="#992600"> + <path id="path636" d="m-76.6,41.2s-4.4,8.8-4.8,12c0,0,0.8-8.8,2-10.8s2.8-1.2,2.8-1.2z"/> + </g> + <g id="g638" fill="#992600"> + <path id="path640" d="m-95,55.2s-3.2,14.4-2.8,17.2c0,0-1.2-11.6-0.8-12.8s3.6-4.4,3.6-4.4z"/> + </g> + <g id="g642" fill="#CCC"> + <path id="path644" d="m-74.2-19.4-0.2,3.2-2.2,0.2s14.2,12.6,14.8,20.2c0,0,0.8-8.2-12.4-23.6z"/> + </g> + <g id="g646" fill="#000"> + <path id="path648" d="m-70.216-18.135c-0.431-0.416-0.212-1.161-0.62-1.421-0.809-0.516,1.298-0.573,1.07-1.289-0.383-1.206-0.196-1.227-0.318-2.503-0.057-0.598,0.531-2.138,0.916-2.578,1.446-1.652,0.122-4.584,1.762-6.135,0.304-0.289,0.68-0.841,0.965-1.259,0.659-0.963,1.843-1.451,2.793-2.279,0.318-0.276,0.117-1.103,0.686-1.011,0.714,0.115,1.955-0.015,1.91,0.826-0.113,2.12-1.442,3.84-2.722,5.508,0.451,0.704-0.007,1.339-0.291,1.896-1.335,2.62-1.146,5.461-1.32,8.301-0.005,0.085-0.312,0.163-0.304,0.216,0.353,2.335,0.937,4.534,1.816,6.763,0.366,0.93,0.837,1.825,0.987,2.752,0.111,0.686,0.214,1.519-0.194,2.224,2.035,2.89,0.726,5.541,1.895,9.072,0.207,0.625,1.899,2.539,1.436,2.378-2.513-0.871-2.625-1.269-2.802-2.022-0.146-0.623-0.476-2-0.713-2.602-0.064-0.164-0.235-2.048-0.313-2.17-1.513-2.382-0.155-2.206-1.525-4.564-1.428-0.68-2.394-1.784-3.517-2.946-0.198-0.204,0.945-0.928,0.764-1.141-1.092-1.289-2.245-2.056-1.909-3.549,0.155-0.69,0.292-1.747-0.452-2.467z"/> + </g> + <g id="g650" fill="#000"> + <path id="path652" d="m-73.8-16.4s0.4,6.8,2.8,8.4,1.2,0.8-2-0.4-2-2-2-2-2.8,0.4-0.4,2.4,6,4.4,4.4,4.4-9.2-4-9.2-6.8-1-6.9-1-6.9,1.1-0.8,5.9-0.7c0,0,1.4,0.7,1.5,1.6z"/> + </g> + <g id="g654" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path656" d="m-74.6,2.2s-8.52-2.791-27,0.6c0,0,9.031-2.078,27.8,0.2,10.3,1.25-0.8-0.8-0.8-0.8z"/> + </g> + <g id="g658" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path660" d="m-72.502,2.129s-8.246-3.518-26.951-1.737c0,0,9.178-1.289,27.679,2.603,10.154,2.136-0.728-0.866-0.728-0.866z"/> + </g> + <g id="g662" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path664" d="m-70.714,2.222s-7.962-4.121-26.747-3.736c0,0,9.248-0.604,27.409,4.654,9.966,2.885-0.662-0.918-0.662-0.918z"/> + </g> + <g id="g666" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path668" d="m-69.444,2.445s-6.824-4.307-23.698-5.405c0,0,8.339,0.17,24.22,6.279,8.716,3.353-0.522-0.874-0.522-0.874z"/> + </g> + <g id="g670" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path672" d="m45.84,12.961s-0.93,0.644-0.716-0.537c0.215-1.181,28.423-14.351,32.037-14.101,0,0-30.248,13.206-31.321,14.638z"/> + </g> + <g id="g674" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path676" d="m42.446,13.6s-0.876,0.715-0.755-0.479,27.208-16.539,30.83-16.573c0,0-29.117,15.541-30.075,17.052z"/> + </g> + <g id="g678" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path680" d="m39.16,14.975s-0.828,0.772-0.786-0.428c0.042-1.199,19.859-16.696,29.671-18.57,0,0-18.03,8.127-28.885,18.998z"/> + </g> + <g id="g682" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path684" d="m36.284,16.838s-0.745,0.694-0.707-0.385c0.038-1.08,17.872-15.027,26.703-16.713,0,0-16.226,7.314-25.996,17.098z"/> + </g> + <g id="g686" fill="#CCC"> + <path id="path688" d="m4.6,164.8s-15.2-2.4,1.6-4c0,0,18-2,22-7.2,0,0,13.6-9.2,16.4-9.6s19.2-4,19.6-6.4,6.4-4.8,8-4,1.6,10-1.2,10.8-21.6,8-28,9.2-18,8.8-22.8,10-15.6,1.2-15.6,1.2z"/> + </g> + <g id="g690" fill="#000"> + <path id="path692" d="m77.6,127.4s-3,1.6-4.2,4.2c0,0-6.4,10.6-20.6,13.8,0,0-23,9-30.8,11,0,0-13.4,5-20.8,4.2,0,0-7,0.2-0.8,1.8,0,0,20.2-2,23.6-3.8,0,0,15.6-5.2,18.6-7.8s21.2-7.6,23.4-9.6,12-10.4,11.6-13.8z"/> + </g> + <g id="g694" fill="#000"> + <path id="path696" d="m18.882,158.91s5.229-0.23,4.076,1.32-3.601,0.68-3.601,0.68l-0.475-2z"/> + </g> + <g id="g698" fill="#000"> + <path id="path700" d="m11.68,160.26s5.228-0.22,4.076,1.33c-1.153,1.55-3.601,0.67-3.601,0.67l-0.475-2z"/> + </g> + <g id="g702" fill="#000"> + <path id="path704" d="m1.251,161.51s5.229-0.23,4.076,1.32-3.601,0.68-3.601,0.68l-0.475-2z"/> + </g> + <g id="g706" fill="#000"> + <path id="path708" d="m-6.383,162.06s5.229-0.23,4.076,1.32-3.601,0.67-3.601,0.67l-0.475-1.99z"/> + </g> + <g id="g710" fill="#000"> + <path id="path712" d="m35.415,151.51s6.96-0.3,5.425,1.76c-1.534,2.07-4.793,0.9-4.793,0.9l-0.632-2.66z"/> + </g> + <g id="g714" fill="#000"> + <path id="path716" d="m45.73,147.09s5.959-3.3,5.425,1.76c-0.27,2.55-4.793,0.9-4.793,0.9l-0.632-2.66z"/> + </g> + <g id="g718" fill="#000"> + <path id="path720" d="m54.862,144.27s7.159-3.7,5.425,1.77c-0.778,2.44-4.794,0.9-4.794,0.9l-0.631-2.67z"/> + </g> + <g id="g722" fill="#000"> + <path id="path724" d="m64.376,139.45s4.359-4.9,5.425,1.76c0.406,2.54-4.793,0.9-4.793,0.9l-0.632-2.66z"/> + </g> + <g id="g726" fill="#000"> + <path id="path728" d="m26.834,156s5.228-0.23,4.076,1.32c-1.153,1.55-3.602,0.68-3.602,0.68l-0.474-2z"/> + </g> + <g id="g730" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path732" d="m62.434,34.603s-0.726,0.665-0.727-0.406c0-1.07,17.484-14.334,26.327-15.718,0,0-16.099,6.729-25.6,16.124z"/> + </g> + <g id="g734" fill="#000"> + <path id="path736" d="m65.4,98.4s22.001,22.4,31.201,26c0,0,9.199,11.2,5.199,37.2,0,0-3.199,7.6-6.399-13.2,0,0,3.2-25.2-8-9.2,0,0-8.401-9.9-2.001-9.6,0,0,3.201,2,3.601,0.4s-7.601-15.2-24.801-29.6,1.2-2,1.2-2z"/> + </g> + <g id="g738" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path740" d="m7,137.2s-0.2-1.8,1.6-1,96,7,127.6,31c0,0-45.199-23.2-129.2-30z"/> + </g> + <g id="g742" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path744" d="m17.4,132.8s-0.2-1.8,1.6-1,138.4-0.2,162,32.2c0,0-22-25.2-163.6-31.2z"/> + </g> + <g id="g746" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path748" d="m29,128.8s-0.2-1.8,1.6-1,175.2-12.2,198.8,20.2c0,0-9.6-25.6-200.4-19.2z"/> + </g> + <g id="g750" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path752" d="m39,124s-0.2-1.8,1.6-1,124-37.8,147.6-5.4c0,0-13.4-24.6-149.2,6.4z"/> + </g> + <g id="g754" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path756" d="m-19,146.8s-0.2-1.8,1.6-1,19.6,3,21.6,41.8c0,0-7.2-42-23.2-40.8z"/> + </g> + <g id="g758" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path760" d="m-27.8,148.4s-0.2-1.8,1.6-1,16-3.8,13.2,35c0,0,1.2-35.2-14.8-34z"/> + </g> + <g id="g762" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path764" d="m-35.8,148.8s-0.2-1.8,1.6-1,17.2,1.4,4.8,23.8c0,0,9.6-24-6.4-22.8z"/> + </g> + <g id="g766" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path768" d="m11.526,104.46s-0.444,2,1.105,0.79c16.068-12.628,48.51-71.53,104.2-77.164,0,0-38.312-12.11-105.3,76.374z"/> + </g> + <g id="g770" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path772" d="m22.726,102.66s-1.363-1.19,0.505-1.81c1.868-0.63,114.31-73.13,153.6-65.164,0,0-27.11-7.51-154.1,66.974z"/> + </g> + <g id="g774" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path776" d="m1.885,108.77s-0.509,1.6,1.202,0.62c8.975-5.12,12.59-62.331,56.167-63.586,0,0-32.411-14.714-57.369,62.966z"/> + </g> + <g id="g778" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path780" d="m-18.038,119.79s-1.077,1.29,0.876,1.03c10.246-1.33,31.651-42.598,76.09-37.519,0,0-31.966-14.346-76.966,36.489z"/> + </g> + <g id="g782" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path784" d="m-6.8,113.67s-0.811,1.47,1.058,0.84c9.799-3.27,22.883-47.885,67.471-51.432,0,0-34.126-7.943-68.529,50.592z"/> + </g> + <g id="g786" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path788" d="m-25.078,124.91s-0.873,1.04,0.709,0.84c8.299-1.08,25.637-34.51,61.633-30.396,0,0-25.893-11.62-62.342,29.556z"/> + </g> + <g id="g790" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path792" d="m-32.677,130.82s-1.005,1.05,0.586,0.93c4.168-0.31,34.806-33.39,53.274-17.89,0,0-12.015-18.721-53.86,16.96z"/> + </g> + <g id="g794" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path796" d="m36.855,98.898s-1.201-1.355,0.731-1.74c1.932-0.384,122.63-58.097,160.59-45.231,0,0-25.94-10.874-161.32,46.971z"/> + </g> + <g id="g798" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path800" d="m3.4,163.2s-0.2-1.8,1.6-1,17.2,1.4,4.8,23.8c0,0,9.6-24-6.4-22.8z"/> + </g> + <g id="g802" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path804" d="m13.8,161.6s-0.2-1.8,1.6-1,19.6,3,21.6,41.8c0,0-7.2-42-23.2-40.8z"/> + </g> + <g id="g806" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path808" d="m20.6,160s-0.2-1.8,1.6-1,26.4,4.2,50,36.6c0,0-35.6-36.8-51.6-35.6z"/> + </g> + <g id="g810" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path812" d="m28.225,157.97s-0.437-1.76,1.453-1.2c1.89,0.55,22.324-1.35,60.421,32.83,0,0-46.175-34.94-61.874-31.63z"/> + </g> + <g id="g814" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path816" d="m38.625,153.57s-0.437-1.76,1.453-1.2c1.89,0.55,36.724,5.05,88.422,40.03,0,0-74.176-42.14-89.875-38.83z"/> + </g> + <g id="g818" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path820" d="m-1.8,142s-0.2-1.8,1.6-1,55.2,3.4,85.6,30.2c0,0-34.901-24.77-87.2-29.2z"/> + </g> + <g id="g822" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path824" d="m-11.8,146s-0.2-1.8,1.6-1,26.4,4.2,50,36.6c0,0-35.6-36.8-51.6-35.6z"/> + </g> + <g id="g826" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path828" d="m49.503,148.96s-0.565-1.72,1.361-1.3c1.926,0.41,36.996,2.34,91.116,33.44,0,0-77.663-34.4-92.477-32.14z"/> + </g> + <g id="g830" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path832" d="m57.903,146.56s-0.565-1.72,1.361-1.3c1.926,0.41,36.996,2.34,91.116,33.44,0,0-77.063-34.8-92.477-32.14z"/> + </g> + <g id="g834" stroke-width="0.1" stroke="#000" fill="#FFF"> + <path id="path836" d="m67.503,141.56s-0.565-1.72,1.361-1.3c1.926,0.41,44.996,4.74,134.72,39.04,0,0-120.66-40.4-136.08-37.74z"/> + </g> + <g id="g838" fill="#000"> + <path id="path840" d="m-43.8,148.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g842" fill="#000"> + <path id="path844" d="m-13,162.4s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g846" fill="#000"> + <path id="path848" d="m-21.8,162s5.2-0.4,4,1.2-3.6,0.8-3.6,0.8l-0.4-2z"/> + </g> + <g id="g850" fill="#000"> + <path id="path852" d="m-117.17,150.18s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/> + </g> + <g id="g854" fill="#000"> + <path id="path856" d="m-115.17,140.58s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/> + </g> + <g id="g858" fill="#000"> + <path id="path860" d="m-122.37,136.18s5.05,1.32,3.39,2.44-3.67-0.42-3.67-0.42l0.28-2.02z"/> + </g> + <g id="g862" fill="#CCC"> + <path id="path864" d="m-42.6,211.2-5.6,2c-2,0-13.2,3.6-18.8,13.6,0,0,12.4-9.6,24.4-15.6z"/> + </g> + <g id="g866" fill="#CCC"> + <path id="path868" d="m45.116,303.85c0.141,0.25,0.196,0.67,0.488,0.69,0.658,0.04,1.891,0.34,1.766-0.29-0.848-4.31-1.722-9.25-5.855-11.05-0.639-0.28-2.081,0.13-2.155,1.02-0.127,1.52-0.244,2.87,0.065,4.33,0.3,1.43,2.458,1.43,3.375,0.05,0.936,1.67,1.368,3.52,2.316,5.25z"/> + </g> + <g id="g870" fill="#CCC"> + <path id="path872" d="m34.038,308.58c0.748,1.41,0.621,3.27,2.036,3.84,0.74,0.29,2.59-0.68,2.172-1.76-0.802-2.06-1.19-4.3-2.579-6.11-0.2-0.26,0.04-0.79-0.12-1.12-0.594-1.22-1.739-1.96-3.147-1.63-1.115,2.2,0.033,4.33,1.555,6.04,0.136,0.15-0.03,0.53,0.083,0.74z"/> + </g> + <g id="g874" fill="#CCC"> + <path id="path876" d="m-5.564,303.39c-0.108-0.38-0.146-0.84,0.019-1.16,0.531-1.03,1.324-2.15,0.987-3.18-0.348-1.05-1.464-0.87-2.114-0.3-1.135,0.99-1.184,2.82-1.875,4.18-0.196,0.38-0.145,0.96-0.586,1.35-0.474,0.42-0.914,1.94-0.818,2.51,0.053,0.32-0.13,10.22,0.092,9.96,0.619-0.73,3.669-10.47,3.738-11.36,0.057-0.73,0.789-1.19,0.557-2z"/> + </g> + <g id="g878" fill="#CCC"> + <path id="path880" d="m-31.202,296.6c2.634-2.5,5.424-5.46,4.982-9.17-0.116-0.98-1.891-0.45-2.078,0.39-0.802,3.63-2.841,6.29-5.409,8.68-2.196,2.05-4.058,8.39-4.293,8.9,3.697-5.26,5.954-8,6.798-8.8z"/> + </g> + <g id="g882" fill="#CCC"> + <path id="path884" d="m-44.776,290.64c0.523-0.38,0.221-0.87,0.438-1.2,0.953-1.46,2.254-2.7,2.272-4.44,0.003-0.28-0.375-0.59-0.71-0.36-0.277,0.18-0.619,0.31-0.727,0.44-2.03,2.45-3.43,5.12-4.873,7.93-0.183,0.36-1.327,4.85-1.014,4.96,0.239,0.09,1.959-4.09,2.169-4.21,1.263-0.68,1.275-2.3,2.445-3.12z"/> + </g> + <g id="g886" fill="#CCC"> + <path id="path888" d="m-28.043,310.18c0.444-0.87,2.02-2.07,1.907-2.96-0.118-0.93,0.35-2.37-0.562-1.68-1.257,0.94-4.706,2.29-4.976,8.1-0.026,0.57,2.948-2.12,3.631-3.46z"/> + </g> + <g id="g890" fill="#CCC"> + <path id="path892" d="m-13.6,293c0.4-0.67,1.108-0.19,1.567-0.46,0.648-0.37,1.259-0.93,1.551-1.58,0.97-2.14,2.739-3.96,2.882-6.36-1.491-1.4-2.17,0.64-2.8,1.6-1.323-1.65-2.322,0.23-3.622,0.75-0.07,0.03-0.283-0.32-0.358-0.29-1.177,0.44-1.857,1.52-2.855,2.3-0.171,0.13-0.576-0.05-0.723,0.09-0.652,0.6-1.625,0.93-1.905,1.61-1.11,2.7-4.25,4.8-6.137,12.34,0.381,0.91,4.512-6.64,4.999-7.34,0.836-1.2,0.954,1.66,2.23,1,0.051-0.03,0.237,0.21,0.371,0.34,0.194-0.28,0.412-0.51,0.8-0.4,0-0.4-0.134-0.96,0.067-1.11,1.237-0.98,1.153-2.05,1.933-3.29,0.458,0.79,1.519,0.07,2,0.8z"/> + </g> + <g id="g894" fill="#CCC"> + <path id="path896" d="m46.2,347.4s7.4-20.4,3-31.6c0,0,11.4,21.6,6.8,32.8,0,0-0.4-10.4-4.4-15.4,0,0-4,12.8-5.4,14.2z"/> + </g> + <g id="g898" fill="#CCC"> + <path id="path900" d="m31.4,344.8s5.4-8.8-2.6-27.2c0,0-0.8,20.4-7.6,31.4,0,0,14.2-20.2,10.2-4.2z"/> + </g> + <g id="g902" fill="#CCC"> + <path id="path904" d="m21.4,342.8s-0.2-20,0.2-23c0,0-3.8,16.6-14,26.2,0,0,14.4-12,13.8-3.2z"/> + </g> + <g id="g906" fill="#CCC"> + <path id="path908" d="m11.8,310.8s6,13.6-4,32c0,0,6.4-12.2,1.6-19.2,0,0,2.6-3.4,2.4-12.8z"/> + </g> + <g id="g910" fill="#CCC"> + <path id="path912" d="m-7.4,342.4s-1-15.6,0.8-17.8c0,0,0.2-6.4-0.2-7.4,0,0,4-6.2,4.2,1.2,0,0,1.4,7.8,4.2,12.4,0,0,3.6,5.4,3.4,11.8,0,0-10-30.2-12.4-0.2z"/> + </g> + <g id="g914" fill="#CCC"> + <path id="path916" d="m-11,314.8s-6.6,10.8-8.4,29.8c0,0-1.4-6.2,2.4-20.6,0,0,4.2-15.4,6-9.2z"/> + </g> + <g id="g918" fill="#CCC"> + <path id="path920" d="m-32.8,334.6s5-5.4,6.4-10.4c0,0,3.6-15.8-2.8-7.2,0,0,0.2,8-8,15.4,0,0,4.8-2.4,4.4,2.2z"/> + </g> + <g id="g922" fill="#CCC"> + <path id="path924" d="m-38.6,329.6s3.4-17.4,4.2-18.2c0,0,1.8-3.4-1-0.2,0,0-8.8,19.2-12.8,25.8,0,0,8-9.2,9.6-7.4z"/> + </g> + <g id="g926" fill="#CCC"> + <path id="path928" d="m-44.4,313s11.6-22.4-10.2,3.4c0,0,11-9.8,10.2-3.4z"/> + </g> + <g id="g930" fill="#CCC"> + <path id="path932" d="m-59.8,298.4s4.8-18.8,7.4-18.6l1.6,1.6s-6,9.6-5.4,19.4c0,0-0.6-9.6-3.6-2.4z"/> + </g> + <g id="g934" fill="#CCC"> + <path id="path936" d="m270.5,287s-12-10-14.5-13.5c0,0,13.5,18.5,13.5,25.5,0,0,2.5-7.5,1-12z"/> + </g> + <g id="g938" fill="#CCC"> + <path id="path940" d="m276,265s-21-15-24.5-22.5c0,0,26.5,29.5,26.5,34,0,0,0.5-9-2-11.5z"/> + </g> + <g id="g942" fill="#CCC"> + <path id="path944" d="m293,111s-12-8-13.5-6c0,0,10.5,6.5,13,15,0,0-1.5-9,0.5-9z"/> + </g> + <g id="g946" fill="#CCC"> + <path id="path948" d="m301.5,191.5-17.5-12s19,17,19.5,21l-2-9z"/> + </g> + <g id="g950" stroke="#000"> + <path id="path952" d="m-89.25,169,22,4.75"/> + </g> + <g id="g954" stroke="#000"> + <path id="path956" d="m-39,331s-0.5-3.5-9.5,7"/> + </g> + <g id="g958" stroke="#000"> + <path id="path960" d="m-33.5,336s2-6.5-4.5-2"/> + </g> + <g id="g962" stroke="#000"> + <path id="path964" d="m20.5,344.5s1.5-11-10,2"/> + </g> + </g> +</svg> diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs new file mode 100644 index 00000000..811fdfb5 --- /dev/null +++ b/examples/svg/src/main.rs @@ -0,0 +1,42 @@ +use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg}; + +pub fn main() { + env_logger::init(); + + Tiger::run(Settings::default()) +} + +#[derive(Default)] +struct Tiger; + +impl Sandbox for Tiger { + type Message = (); + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("SVG - Iced") + } + + 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) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml new file mode 100644 index 00000000..f945cde5 --- /dev/null +++ b/examples/todos/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["async-std"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +async-std = "1.0" +directories = "2.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3", features = ["Window", "Storage"] } +wasm-timer = "0.2" + +[package.metadata.deb] +assets = [ + ["target/release/todos", "usr/bin/iced-todos", "755"], + ["iced-todos.desktop", "usr/share/applications/", "644"], +] diff --git a/examples/todos/README.md b/examples/todos/README.md new file mode 100644 index 00000000..9c2598b9 --- /dev/null +++ b/examples/todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + +<div align="center"> + <a href="https://gfycat.com/littlesanehalicore"> + <img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px"> + </a> +</div> + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ diff --git a/examples/resources/icons.ttf b/examples/todos/fonts/icons.ttf Binary files differindex 4498299d..4498299d 100644 --- a/examples/resources/icons.ttf +++ b/examples/todos/fonts/icons.ttf diff --git a/examples/todos/iced-todos.desktop b/examples/todos/iced-todos.desktop new file mode 100644 index 00000000..dd7ce53d --- /dev/null +++ b/examples/todos/iced-todos.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Name=Todos - Iced +Exec=iced-todos +Type=Application diff --git a/examples/todos.rs b/examples/todos/src/main.rs index af8a37a8..7e866b19 100644 --- a/examples/todos.rs +++ b/examples/todos/src/main.rs @@ -1,7 +1,7 @@ use iced::{ - button, scrollable, text_input, Align, Application, Background, Button, - Checkbox, Color, Column, Command, Container, Element, Font, - HorizontalAlignment, Length, Row, Scrollable, Settings, Text, TextInput, + button, scrollable, text_input, Align, Application, Button, Checkbox, + Column, Command, Container, Element, Font, HorizontalAlignment, Length, + Row, Scrollable, Settings, Text, TextInput, }; use serde::{Deserialize, Serialize}; @@ -38,6 +38,7 @@ enum Message { } impl Application for Todos { + type Executor = iced::executor::Default; type Message = Message; fn new() -> (Todos, Command<Message>) { @@ -146,6 +147,7 @@ impl Application for Todos { .. }) => { let title = Text::new("todos") + .width(Length::Fill) .size(100) .color([0.5, 0.5, 0.5]) .horizontal_alignment(HorizontalAlignment::Center); @@ -284,19 +286,18 @@ impl Task { self.completed, &self.description, TaskMessage::Completed, - ); + ) + .width(Length::Fill); Row::new() .spacing(20) .align_items(Align::Center) .push(checkbox) .push( - Button::new( - edit_button, - edit_icon().color([0.5, 0.5, 0.5]), - ) - .on_press(TaskMessage::Edit) - .padding(10), + Button::new(edit_button, edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(style::Button::Icon), ) .into() } @@ -322,17 +323,12 @@ impl Task { delete_button, Row::new() .spacing(10) - .push(delete_icon().color(Color::WHITE)) - .push( - Text::new("Delete") - .width(Length::Shrink) - .color(Color::WHITE), - ), + .push(delete_icon()) + .push(Text::new("Delete")), ) .on_press(TaskMessage::Delete) .padding(10) - .border_radius(5) - .background(Background::Color([0.8, 0.2, 0.2].into())), + .style(style::Button::Destructive), ) .into() } @@ -358,18 +354,13 @@ impl Controls { let tasks_left = tasks.iter().filter(|task| !task.completed).count(); let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16).width(Length::Shrink); - let button = if filter == current_filter { - Button::new(state, label.color(Color::WHITE)) - .background(Background::Color([0.2, 0.2, 0.7].into())) - } else { - Button::new(state, label) - }; - - button - .on_press(Message::FilterChanged(filter)) - .padding(8) - .border_radius(10) + let label = Text::new(label).size(16); + let button = + Button::new(state, label).style(style::Button::Filter { + selected: filter == current_filter, + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) }; Row::new() @@ -381,6 +372,7 @@ impl Controls { tasks_left, if tasks_left == 1 { "task" } else { "tasks" } )) + .width(Length::Fill) .size(16), ) .push( @@ -447,6 +439,7 @@ fn loading_message() -> Element<'static, Message> { fn empty_message(message: &str) -> Element<'static, Message> { Container::new( Text::new(message) + .width(Length::Fill) .size(25) .horizontal_alignment(HorizontalAlignment::Center) .color([0.7, 0.7, 0.7]), @@ -460,7 +453,7 @@ fn empty_message(message: &str) -> Element<'static, Message> { // Fonts const ICONS: Font = Font::External { name: "Icons", - bytes: include_bytes!("resources/icons.ttf"), + bytes: include_bytes!("../fonts/icons.ttf"), }; fn icon(unicode: char) -> Text { @@ -501,6 +494,7 @@ enum SaveError { FormatError, } +#[cfg(not(target_arch = "wasm32"))] impl SavedState { fn path() -> std::path::PathBuf { let mut path = if let Some(project_dirs) = @@ -517,21 +511,23 @@ impl SavedState { } async fn load() -> Result<SavedState, LoadError> { - use std::io::Read; + use async_std::prelude::*; let mut contents = String::new(); - let mut file = std::fs::File::open(Self::path()) + let mut file = async_std::fs::File::open(Self::path()) + .await .map_err(|_| LoadError::FileError)?; file.read_to_string(&mut contents) + .await .map_err(|_| LoadError::FileError)?; serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) } async fn save(self) -> Result<(), SaveError> { - use std::io::Write; + use async_std::prelude::*; let json = serde_json::to_string_pretty(&self) .map_err(|_| SaveError::FormatError)?; @@ -539,21 +535,119 @@ impl SavedState { let path = Self::path(); if let Some(dir) = path.parent() { - std::fs::create_dir_all(dir) + async_std::fs::create_dir_all(dir) + .await .map_err(|_| SaveError::DirectoryError)?; } - let mut file = - std::fs::File::create(path).map_err(|_| SaveError::FileError)?; + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::FileError)?; - file.write_all(json.as_bytes()) - .map_err(|_| SaveError::WriteError)?; + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::WriteError)?; + } // This is a simple way to save at most once every couple seconds - // We will be able to get rid of it once we implement event - // subscriptions - std::thread::sleep(std::time::Duration::from_secs(2)); + async_std::task::sleep(std::time::Duration::from_secs(2)).await; Ok(()) } } + +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option<web_sys::Storage> { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result<SavedState, LoadError> { + let storage = Self::storage().ok_or(LoadError::FileError)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::FileError)? + .ok_or(LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::FileError)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::WriteError)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Filter { selected: bool }, + Icon, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + match self { + Button::Filter { selected } => { + if *selected { + button::Style { + background: Some(Background::Color( + Color::from_rgb(0.2, 0.2, 0.7), + )), + border_radius: 10, + text_color: Color::WHITE, + ..button::Style::default() + } + } else { + button::Style::default() + } + } + Button::Icon => button::Style { + text_color: Color::from_rgb(0.5, 0.5, 0.5), + ..button::Style::default() + }, + Button::Destructive => button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.8, 0.2, 0.2, + ))), + border_radius: 5, + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 1.0), + ..button::Style::default() + }, + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + button::Style { + text_color: match self { + Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), + Button::Filter { selected } if !selected => { + Color::from_rgb(0.2, 0.2, 0.7) + } + _ => active.text_color, + }, + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + } +} diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml new file mode 100644 index 00000000..96749e90 --- /dev/null +++ b/examples/tour/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tour" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["image", "debug"] } +env_logger = "0.7" diff --git a/examples/tour/README.md b/examples/tour/README.md new file mode 100644 index 00000000..f380931a --- /dev/null +++ b/examples/tour/README.md @@ -0,0 +1,28 @@ +## Tour + +A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced. + +The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. + +<div align="center"> + <a href="https://gfycat.com/politeadorableiberianmole"> + <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> + </a> +</div> + +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: ../../web +[`winit`]: https://github.com/rust-windowing/winit +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs + +You can run the native version with `cargo run`: +``` +cargo run --package tour +``` + +The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! + +[the usage instructions of `iced_web`]: ../../web#usage diff --git a/examples/resources/ferris.png b/examples/tour/images/ferris.png Binary files differindex ebce1a14..ebce1a14 100644 --- a/examples/resources/ferris.png +++ b/examples/tour/images/ferris.png diff --git a/examples/tour.rs b/examples/tour/src/main.rs index 0121c3bd..800254ed 100644 --- a/examples/tour.rs +++ b/examples/tour/src/main.rs @@ -1,7 +1,7 @@ use iced::{ - button, scrollable, slider, text_input, Background, Button, Checkbox, - Color, Column, Container, Element, HorizontalAlignment, Image, Length, - Radio, Row, Sandbox, Scrollable, Settings, Slider, Text, TextInput, + button, scrollable, slider, text_input, Button, Checkbox, Color, Column, + Container, Element, HorizontalAlignment, Image, Length, Radio, Row, + Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, }; pub fn main() { @@ -62,17 +62,19 @@ impl Sandbox for Tour { if steps.has_previous() { controls = controls.push( - secondary_button(back_button, "Back") - .on_press(Message::BackPressed), + button(back_button, "Back") + .on_press(Message::BackPressed) + .style(style::Button::Secondary), ); } - controls = controls.push(Column::new()); + controls = controls.push(Space::with_width(Length::Fill)); if steps.can_continue() { controls = controls.push( - primary_button(next_button, "Next") - .on_press(Message::NextPressed), + button(next_button, "Next") + .on_press(Message::NextPressed) + .style(style::Button::Primary), ); } @@ -140,6 +142,7 @@ impl Steps { Step::Scrollable, Step::TextInput { value: String::new(), + is_secure: false, state: text_input::State::new(), }, Step::Debugger, @@ -210,6 +213,7 @@ enum Step { Scrollable, TextInput { value: String, + is_secure: bool, state: text_input::State, }, Debugger, @@ -226,6 +230,7 @@ pub enum StepMessage { LanguageSelected(Language), ImageWidthChanged(f32), InputChanged(String), + ToggleSecureInput(bool), DebugToggled(bool), } @@ -277,6 +282,11 @@ impl<'a> Step { *value = new_value; } } + StepMessage::ToggleSecureInput(toggle) => { + if let Step::TextInput { is_secure, .. } = self { + *is_secure = toggle; + } + } }; } @@ -328,7 +338,11 @@ impl<'a> Step { spacing, } => Self::rows_and_columns(*layout, spacing_slider, *spacing), Step::Scrollable => Self::scrollable(), - Step::TextInput { value, state } => Self::text_input(value, state), + Step::TextInput { + value, + is_secure, + state, + } => Self::text_input(value, *is_secure, state), Step::Debugger => Self::debugger(debug), Step::End => Self::end(), } @@ -389,6 +403,7 @@ impl<'a> Step { )) .push( Text::new(&value.to_string()) + .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Center), ) } @@ -435,6 +450,7 @@ impl<'a> Step { )) .push( Text::new(&format!("{} px", spacing)) + .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Center), ); @@ -549,6 +565,7 @@ impl<'a> Step { )) .push( Text::new(&format!("Width: {} px", width.to_string())) + .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Center), ) } @@ -568,6 +585,7 @@ impl<'a> Step { .push(Column::new().height(Length::Units(4096))) .push( Text::new("You are halfway there!") + .width(Length::Fill) .size(30) .horizontal_alignment(HorizontalAlignment::Center), ) @@ -575,6 +593,7 @@ impl<'a> Step { .push(ferris(300)) .push( Text::new("You made it!") + .width(Length::Fill) .size(50) .horizontal_alignment(HorizontalAlignment::Center), ) @@ -582,22 +601,31 @@ impl<'a> Step { fn text_input( value: &str, + is_secure: bool, state: &'a mut text_input::State, ) -> Column<'a, StepMessage> { + let text_input = TextInput::new( + state, + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); Self::container("Text input") .push(Text::new( "Use a text input to ask for different kinds of information.", )) - .push( - TextInput::new( - state, - "Type something to continue...", - value, - StepMessage::InputChanged, - ) - .padding(10) - .size(30), - ) + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(Checkbox::new( + is_secure, + "Enable password mode", + StepMessage::ToggleSecureInput, + )) .push(Text::new( "A text input produces a message every time it changes. It is \ very easy to keep track of its contents:", @@ -608,6 +636,7 @@ impl<'a> Step { } else { value }) + .width(Length::Fill) .horizontal_alignment(HorizontalAlignment::Center), ) } @@ -652,10 +681,10 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { // This should go away once we unify resource loading on native // platforms if cfg!(target_arch = "wasm32") { - Image::new("resources/ferris.png") + Image::new("images/ferris.png") } else { Image::new(format!( - "{}/examples/resources/ferris.png", + "{}/images/ferris.png", env!("CARGO_MANIFEST_DIR") )) } @@ -671,39 +700,12 @@ fn button<'a, Message>( ) -> Button<'a, Message> { Button::new( state, - Text::new(label) - .color(Color::WHITE) - .horizontal_alignment(HorizontalAlignment::Center), + Text::new(label).horizontal_alignment(HorizontalAlignment::Center), ) .padding(12) - .border_radius(12) .min_width(100) } -fn primary_button<'a, Message>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - button(state, label).background(Background::Color(Color { - r: 0.11, - g: 0.42, - b: 0.87, - a: 1.0, - })) -} - -fn secondary_button<'a, Message>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - button(state, label).background(Background::Color(Color { - r: 0.4, - g: 0.4, - b: 0.4, - a: 1.0, - })) -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { Rust, @@ -746,15 +748,34 @@ pub enum Layout { Column, } -// This should be gracefully handled by Iced in the future. Probably using our -// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at -// some point. -#[cfg(target_arch = "wasm32")] -mod wasm { - use wasm_bindgen::prelude::*; +mod style { + use iced::{button, Background, Color, Vector}; - #[wasm_bindgen(start)] - pub fn run() { - super::main() + pub enum Button { + Primary, + Secondary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } } } diff --git a/futures/Cargo.toml b/futures/Cargo.toml new file mode 100644 index 00000000..483e60cb --- /dev/null +++ b/futures/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "iced_futures" +version = "0.1.0-alpha" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "Commands, subscriptions, and runtimes for Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_futures" +keywords = ["gui", "ui", "graphics", "interface", "futures"] +categories = ["gui"] + +[features] +thread-pool = ["futures/thread-pool"] + +[dependencies] +log = "0.4" + +[dependencies.futures] +version = "0.3" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] +version = "0.2" +optional = true +features = ["rt-core", "rt-threaded"] + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std] +version = "1.0" +optional = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" diff --git a/futures/src/command.rs b/futures/src/command.rs new file mode 100644 index 00000000..26f58fde --- /dev/null +++ b/futures/src/command.rs @@ -0,0 +1,11 @@ +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::Command; + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(target_arch = "wasm32")] +pub use web::Command; diff --git a/core/src/command.rs b/futures/src/command/native.rs index 14b48b5b..38cb4e06 100644 --- a/core/src/command.rs +++ b/futures/src/command/native.rs @@ -34,15 +34,43 @@ impl<T> Command<T> { } } - /// Creates a [`Command`] that performs the actions of all the givens - /// futures. + /// Applies a transformation to the result of a [`Command`]. /// - /// Once this command is run, all the futures will be exectued at once. + /// [`Command`]: struct.Command.html + pub fn map<A>( + mut self, + f: impl Fn(T) -> A + 'static + Send + Sync, + ) -> Command<A> + where + T: 'static, + { + let f = std::sync::Arc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + future.map(move |result| f(result)).boxed() + }) + .collect(), + } + } + + /// Creates a [`Command`] that performs the actions of all the given + /// commands. + /// + /// Once this command is run, all the commands will be executed at once. /// /// [`Command`]: struct.Command.html - pub fn batch(commands: impl Iterator<Item = Command<T>>) -> Self { + pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self { Self { - futures: commands.flat_map(|command| command.futures).collect(), + futures: commands + .into_iter() + .flat_map(|command| command.futures) + .collect(), } } diff --git a/futures/src/command/web.rs b/futures/src/command/web.rs new file mode 100644 index 00000000..11b46b90 --- /dev/null +++ b/futures/src/command/web.rs @@ -0,0 +1,101 @@ +use futures::future::{Future, FutureExt}; +use std::pin::Pin; + +/// A collection of async operations. +/// +/// You should be able to turn a future easily into a [`Command`], either by +/// using the `From` trait or [`Command::perform`]. +/// +/// [`Command`]: struct.Command.html +pub struct Command<T> { + futures: Vec<Pin<Box<dyn Future<Output = T> + 'static>>>, +} + +impl<T> Command<T> { + /// Creates an empty [`Command`]. + /// + /// In other words, a [`Command`] that does nothing. + /// + /// [`Command`]: struct.Command.html + pub fn none() -> Self { + Self { + futures: Vec::new(), + } + } + + /// Creates a [`Command`] that performs the action of the given future. + /// + /// [`Command`]: struct.Command.html + pub fn perform<A>( + future: impl Future<Output = T> + 'static, + f: impl Fn(T) -> A + 'static, + ) -> Command<A> { + Command { + futures: vec![future.map(f).boxed_local()], + } + } + + /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: struct.Command.html + pub fn map<A>( + mut self, + f: impl Fn(T) -> A + 'static + Send + Sync + Unpin, + ) -> Command<A> + where + T: 'static, + { + let f = std::sync::Arc::new(f); + + Command { + futures: self + .futures + .drain(..) + .map(|future| { + let f = f.clone(); + + future.map(move |result| f(result)).boxed_local() + }) + .collect(), + } + } + + /// Creates a [`Command`] that performs the actions of all the given + /// commands. + /// + /// Once this command is run, all the commands will be executed at once. + /// + /// [`Command`]: struct.Command.html + pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self { + Self { + futures: commands + .into_iter() + .flat_map(|command| command.futures) + .collect(), + } + } + + /// Converts a [`Command`] into its underlying list of futures. + /// + /// [`Command`]: struct.Command.html + pub fn futures(self) -> Vec<Pin<Box<dyn Future<Output = T> + 'static>>> { + self.futures + } +} + +impl<T, A> From<A> for Command<T> +where + A: Future<Output = T> + 'static, +{ + fn from(future: A) -> Self { + Self { + futures: vec![future.boxed_local()], + } + } +} + +impl<T> std::fmt::Debug for Command<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command").finish() + } +} diff --git a/futures/src/executor.rs b/futures/src/executor.rs new file mode 100644 index 00000000..5378c0b3 --- /dev/null +++ b/futures/src/executor.rs @@ -0,0 +1,62 @@ +//! Choose your preferred executor to power a runtime. +mod null; + +#[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))] +mod thread_pool; + +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] +mod tokio; + +#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] +mod async_std; + +#[cfg(target_arch = "wasm32")] +mod wasm_bindgen; + +pub use null::Null; + +#[cfg(all(not(target_arch = "wasm32"), feature = "thread-pool"))] +pub use thread_pool::ThreadPool; + +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] +pub use self::tokio::Tokio; + +#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] +pub use self::async_std::AsyncStd; + +#[cfg(target_arch = "wasm32")] +pub use wasm_bindgen::WasmBindgen; + +use futures::Future; + +/// A type that can run futures. +pub trait Executor: Sized { + /// Creates a new [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html + fn new() -> Result<Self, futures::io::Error> + where + Self: Sized; + + /// Spawns a future in the [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html + #[cfg(not(target_arch = "wasm32"))] + fn spawn(&self, future: impl Future<Output = ()> + Send + 'static); + + /// Spawns a local future in the [`Executor`]. + /// + /// [`Executor`]: trait.Executor.html + #[cfg(target_arch = "wasm32")] + fn spawn(&self, future: impl Future<Output = ()> + 'static); + + /// Runs the given closure inside the [`Executor`]. + /// + /// Some executors, like `tokio`, require some global state to be in place + /// before creating futures. This method can be leveraged to set up this + /// global state, call a function, restore the state, and obtain the result + /// of the call. + fn enter<R>(&self, f: impl FnOnce() -> R) -> R { + f() + } +} diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs new file mode 100644 index 00000000..27949e31 --- /dev/null +++ b/futures/src/executor/async_std.rs @@ -0,0 +1,17 @@ +use crate::Executor; + +use futures::Future; + +/// An `async-std` runtime. +#[derive(Debug)] +pub struct AsyncStd; + +impl Executor for AsyncStd { + fn new() -> Result<Self, futures::io::Error> { + Ok(Self) + } + + fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) { + let _ = async_std::task::spawn(future); + } +} diff --git a/futures/src/executor/null.rs b/futures/src/executor/null.rs new file mode 100644 index 00000000..65e2e2df --- /dev/null +++ b/futures/src/executor/null.rs @@ -0,0 +1,19 @@ +use crate::Executor; + +use futures::Future; + +/// An executor that drops all the futures, instead of spawning them. +#[derive(Debug)] +pub struct Null; + +impl Executor for Null { + fn new() -> Result<Self, futures::io::Error> { + Ok(Self) + } + + #[cfg(not(target_arch = "wasm32"))] + fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {} + + #[cfg(target_arch = "wasm32")] + fn spawn(&self, _future: impl Future<Output = ()> + 'static) {} +} diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs new file mode 100644 index 00000000..1ec5bf69 --- /dev/null +++ b/futures/src/executor/thread_pool.rs @@ -0,0 +1,16 @@ +use crate::Executor; + +use futures::Future; + +/// A thread pool runtime for futures. +pub type ThreadPool = futures::executor::ThreadPool; + +impl Executor for futures::executor::ThreadPool { + fn new() -> Result<Self, futures::io::Error> { + futures::executor::ThreadPool::new() + } + + fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) { + self.spawn_ok(future); + } +} diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs new file mode 100644 index 00000000..20802ceb --- /dev/null +++ b/futures/src/executor/tokio.rs @@ -0,0 +1,20 @@ +use crate::Executor; + +use futures::Future; + +/// A `tokio` runtime. +pub type Tokio = tokio::runtime::Runtime; + +impl Executor for Tokio { + fn new() -> Result<Self, futures::io::Error> { + tokio::runtime::Runtime::new() + } + + fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) { + let _ = tokio::runtime::Runtime::spawn(self, future); + } + + fn enter<R>(&self, f: impl FnOnce() -> R) -> R { + tokio::runtime::Runtime::enter(self, f) + } +} diff --git a/futures/src/executor/wasm_bindgen.rs b/futures/src/executor/wasm_bindgen.rs new file mode 100644 index 00000000..94d694c8 --- /dev/null +++ b/futures/src/executor/wasm_bindgen.rs @@ -0,0 +1,15 @@ +use crate::Executor; + +/// A `wasm-bindgen-futures` runtime. +#[derive(Debug)] +pub struct WasmBindgen; + +impl Executor for WasmBindgen { + fn new() -> Result<Self, futures::io::Error> { + Ok(Self) + } + + fn spawn(&self, future: impl futures::Future<Output = ()> + 'static) { + wasm_bindgen_futures::spawn_local(future); + } +} diff --git a/futures/src/lib.rs b/futures/src/lib.rs new file mode 100644 index 00000000..c25c0853 --- /dev/null +++ b/futures/src/lib.rs @@ -0,0 +1,18 @@ +//! Asynchronous tasks for GUI programming, inspired by Elm. +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] +pub use futures; + +mod command; +mod runtime; + +pub mod executor; +pub mod subscription; + +pub use command::Command; +pub use executor::Executor; +pub use runtime::Runtime; +pub use subscription::Subscription; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs new file mode 100644 index 00000000..d204670b --- /dev/null +++ b/futures/src/runtime.rs @@ -0,0 +1,124 @@ +//! Run commands and keep track of subscriptions. +use crate::{subscription, Command, Executor, Subscription}; + +use futures::{channel::mpsc, Sink}; +use std::marker::PhantomData; + +/// A batteries-included runtime of commands and subscriptions. +/// +/// If you have an [`Executor`], a [`Runtime`] can be leveraged to run any +/// [`Command`] or [`Subscription`] and get notified of the results! +/// +/// [`Runtime`]: struct.Runtime.html +/// [`Executor`]: executor/trait.Executor.html +/// [`Command`]: struct.Command.html +/// [`Subscription`]: subscription/struct.Subscription.html +#[derive(Debug)] +pub struct Runtime<Hasher, Event, Executor, Sender, Message> { + executor: Executor, + sender: Sender, + subscriptions: subscription::Tracker<Hasher, Event>, + _message: PhantomData<Message>, +} + +impl<Hasher, Event, Executor, Sender, Message> + Runtime<Hasher, Event, Executor, Sender, Message> +where + Hasher: std::hash::Hasher + Default, + Event: Send + Clone + 'static, + Executor: self::Executor, + Sender: + Sink<Message, Error = mpsc::SendError> + Unpin + Send + Clone + 'static, + Message: Send + 'static, +{ + /// Creates a new empty [`Runtime`]. + /// + /// You need to provide: + /// - an [`Executor`] to spawn futures + /// - a `Sender` implementing `Sink` to receive the results + /// + /// [`Runtime`]: struct.Runtime.html + pub fn new(executor: Executor, sender: Sender) -> Self { + Self { + executor, + sender, + subscriptions: subscription::Tracker::new(), + _message: PhantomData, + } + } + + /// Runs the given closure inside the [`Executor`] of the [`Runtime`]. + /// + /// See [`Executor::enter`] to learn more. + /// + /// [`Executor`]: executor/trait.Executor.html + /// [`Runtime`]: struct.Runtime.html + /// [`Executor::enter`]: executor/trait.Executor.html#method.enter + pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R { + self.executor.enter(f) + } + + /// Spawns a [`Command`] in the [`Runtime`]. + /// + /// The resulting `Message` will be forwarded to the `Sender` of the + /// [`Runtime`]. + /// + /// [`Command`]: struct.Command.html + /// [`Runtime`]: struct.Runtime.html + pub fn spawn(&mut self, command: Command<Message>) { + use futures::{FutureExt, SinkExt}; + + let futures = command.futures(); + + for future in futures { + let mut sender = self.sender.clone(); + + let future = future.then(|message| async move { + let _ = sender.send(message).await; + + () + }); + + self.executor.spawn(future); + } + } + + /// Tracks a [`Subscription`] in the [`Runtime`]. + /// + /// It will spawn new streams or close old ones as necessary! See + /// [`Tracker::update`] to learn more about this! + /// + /// [`Subscription`]: subscription/struct.Subscription.html + /// [`Runtime`]: struct.Runtime.html + /// [`Tracker::update`]: subscription/struct.Tracker.html#method.update + pub fn track( + &mut self, + subscription: Subscription<Hasher, Event, Message>, + ) { + let Runtime { + executor, + subscriptions, + sender, + .. + } = self; + + let futures = executor + .enter(|| subscriptions.update(subscription, sender.clone())); + + for future in futures { + executor.spawn(future); + } + } + + /// Broadcasts an event to all the subscriptions currently alive in the + /// [`Runtime`]. + /// + /// See [`Tracker::broadcast`] to learn more. + /// + /// [`Runtime`]: struct.Runtime.html + /// [`Tracker::broadcast`]: + /// subscription/struct.Tracker.html#method.broadcast + pub fn broadcast(&mut self, event: Event) { + self.subscriptions.broadcast(event); + } +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs new file mode 100644 index 00000000..b68444cd --- /dev/null +++ b/futures/src/subscription.rs @@ -0,0 +1,189 @@ +//! Listen to external events in your application. +mod tracker; + +pub use tracker::Tracker; + +use futures::stream::BoxStream; + +/// A request to listen to external events. +/// +/// Besides performing async actions on demand with [`Command`], most +/// applications also need to listen to external events passively. +/// +/// A [`Subscription`] is normally provided to some runtime, like a [`Command`], +/// and it will generate events as long as the user keeps requesting it. +/// +/// For instance, you can use a [`Subscription`] to listen to a WebSocket +/// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// This type is normally aliased by runtimes with a specific `Event` and/or +/// `Hasher`. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: struct.Subscription.html +pub struct Subscription<Hasher, Event, Output> { + recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>, +} + +impl<H, E, O> Subscription<H, E, O> +where + H: std::hash::Hasher, +{ + /// Returns an empty [`Subscription`] that will not produce any output. + /// + /// [`Subscription`]: struct.Subscription.html + pub fn none() -> Self { + Self { + recipes: Vec::new(), + } + } + + /// Creates a [`Subscription`] from a [`Recipe`] describing it. + /// + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html + pub fn from_recipe( + recipe: impl Recipe<H, E, Output = O> + 'static, + ) -> Self { + Self { + recipes: vec![Box::new(recipe)], + } + } + + /// Batches all the provided subscriptions and returns the resulting + /// [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + pub fn batch( + subscriptions: impl IntoIterator<Item = Subscription<H, E, O>>, + ) -> Self { + Self { + recipes: subscriptions + .into_iter() + .flat_map(|subscription| subscription.recipes) + .collect(), + } + } + + /// Returns the different recipes of the [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + pub fn recipes(self) -> Vec<Box<dyn Recipe<H, E, Output = O>>> { + self.recipes + } + + /// Transforms the [`Subscription`] output with the given function. + /// + /// [`Subscription`]: struct.Subscription.html + pub fn map<A>( + mut self, + f: impl Fn(O) -> A + Send + Sync + 'static, + ) -> Subscription<H, E, A> + where + H: 'static, + E: 'static, + O: 'static, + A: 'static, + { + let function = std::sync::Arc::new(f); + + Subscription { + recipes: self + .recipes + .drain(..) + .map(|recipe| { + Box::new(Map::new(recipe, function.clone())) + as Box<dyn Recipe<H, E, Output = A>> + }) + .collect(), + } + } +} + +impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} + +/// The description of a [`Subscription`]. +/// +/// A [`Recipe`] is the internal definition of a [`Subscription`]. It is used +/// by runtimes to run and identify subscriptions. You can use it to create your +/// own! +/// +/// [`Subscription`]: struct.Subscription.html +/// [`Recipe`]: trait.Recipe.html +pub trait Recipe<Hasher: std::hash::Hasher, Event> { + /// The events that will be produced by a [`Subscription`] with this + /// [`Recipe`]. + /// + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html + type Output; + + /// Hashes the [`Recipe`]. + /// + /// This is used by runtimes to uniquely identify a [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html + fn hash(&self, state: &mut Hasher); + + /// Executes the [`Recipe`] and produces the stream of events of its + /// [`Subscription`]. + /// + /// It receives some stream of generic events, which is normally defined by + /// shells. + /// + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html + fn stream( + self: Box<Self>, + input: BoxStream<'static, Event>, + ) -> BoxStream<'static, Self::Output>; +} + +struct Map<Hasher, Event, A, B> { + recipe: Box<dyn Recipe<Hasher, Event, Output = A>>, + mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>, +} + +impl<H, E, A, B> Map<H, E, A, B> { + fn new( + recipe: Box<dyn Recipe<H, E, Output = A>>, + mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync + 'static>, + ) -> Self { + Map { recipe, mapper } + } +} + +impl<H, E, A, B> Recipe<H, E> for Map<H, E, A, B> +where + A: 'static, + B: 'static, + H: std::hash::Hasher, +{ + type Output = B; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::<B>().hash(state); + self.recipe.hash(state); + } + + fn stream( + self: Box<Self>, + input: BoxStream<'static, E>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::StreamExt; + + let mapper = self.mapper; + + self.recipe + .stream(input) + .map(move |element| mapper(element)) + .boxed() + } +} diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs new file mode 100644 index 00000000..cfa36170 --- /dev/null +++ b/futures/src/subscription/tracker.rs @@ -0,0 +1,147 @@ +use crate::Subscription; + +use futures::{channel::mpsc, future::BoxFuture, sink::Sink}; +use std::{collections::HashMap, marker::PhantomData}; + +/// A registry of subscription streams. +/// +/// If you have an application that continuously returns a [`Subscription`], +/// you can use a [`Tracker`] to keep track of the different recipes and keep +/// its executions alive. +#[derive(Debug)] +pub struct Tracker<Hasher, Event> { + subscriptions: HashMap<u64, Execution<Event>>, + _hasher: PhantomData<Hasher>, +} + +#[derive(Debug)] +pub struct Execution<Event> { + _cancel: futures::channel::oneshot::Sender<()>, + listener: Option<futures::channel::mpsc::Sender<Event>>, +} + +impl<Hasher, Event> Tracker<Hasher, Event> +where + Hasher: std::hash::Hasher + Default, + Event: 'static + Send + Clone, +{ + /// Creates a new empty [`Tracker`]. + /// + /// [`Tracker`]: struct.Tracker.html + pub fn new() -> Self { + Self { + subscriptions: HashMap::new(), + _hasher: PhantomData, + } + } + + /// Updates the [`Tracker`] with the given [`Subscription`]. + /// + /// A [`Subscription`] can cause new streams to be spawned or old streams + /// to be closed. + /// + /// The [`Tracker`] keeps track of these streams between calls to this + /// method: + /// + /// - If the provided [`Subscription`] contains a new [`Recipe`] that is + /// currently not being run, it will spawn a new stream and keep it alive. + /// - On the other hand, if a [`Recipe`] is currently in execution and the + /// provided [`Subscription`] does not contain it anymore, then the + /// [`Tracker`] will close and drop the relevant stream. + /// + /// It returns a list of futures that need to be spawned to materialize + /// the [`Tracker`] changes. + /// + /// [`Tracker`]: struct.Tracker.html + /// [`Subscription`]: struct.Subscription.html + /// [`Recipe`]: trait.Recipe.html + pub fn update<Message, Receiver>( + &mut self, + subscription: Subscription<Hasher, Event, Message>, + receiver: Receiver, + ) -> Vec<BoxFuture<'static, ()>> + where + Message: 'static + Send, + Receiver: 'static + + Sink<Message, Error = mpsc::SendError> + + Unpin + + Send + + Clone, + { + use futures::{future::FutureExt, stream::StreamExt}; + + let mut futures = Vec::new(); + + let recipes = subscription.recipes(); + let mut alive = std::collections::HashSet::new(); + + for recipe in recipes { + let id = { + let mut hasher = Hasher::default(); + recipe.hash(&mut hasher); + + hasher.finish() + }; + + let _ = alive.insert(id); + + if self.subscriptions.contains_key(&id) { + continue; + } + + let (cancel, cancelled) = futures::channel::oneshot::channel(); + + // TODO: Use bus if/when it supports async + let (event_sender, event_receiver) = + futures::channel::mpsc::channel(100); + + let stream = recipe.stream(event_receiver.boxed()); + + let future = futures::future::select( + cancelled, + stream.map(Ok).forward(receiver.clone()), + ) + .map(|_| ()); + + let _ = self.subscriptions.insert( + id, + Execution { + _cancel: cancel, + listener: if event_sender.is_closed() { + None + } else { + Some(event_sender) + }, + }, + ); + + futures.push(future.boxed()); + } + + self.subscriptions.retain(|id, _| alive.contains(&id)); + + futures + } + + /// Broadcasts an event to the subscriptions currently alive. + /// + /// A subscription's [`Recipe::stream`] always receives a stream of events + /// as input. This stream can be used by some subscription to listen to + /// shell events. + /// + /// This method publishes the given event to all the subscription streams + /// currently open. + pub fn broadcast(&mut self, event: Event) { + self.subscriptions + .values_mut() + .filter_map(|connection| connection.listener.as_mut()) + .for_each(|listener| { + if let Err(error) = listener.try_send(event.clone()) { + log::error!( + "Error sending event to subscription: {:?}", + error + ); + } + }); + } +} diff --git a/native/CHANGELOG.md b/native/CHANGELOG.md index d3638207..df8852b7 100644 --- a/native/CHANGELOG.md +++ b/native/CHANGELOG.md @@ -5,6 +5,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `image::Handle` type with `from_path` and `from_memory` methods. [#90] +- `image::Data` enum representing different kinds of image data. [#90] +- `text_input::Renderer::measure_value` required method to measure the width of a `TextInput` value. [#108] +- Click-based cursor positioning for `TextInput`. [#108] +- `Home` and `End` keys support for `TextInput`. [#108] +- `Ctrl+Left` and `Ctrl+Right` cursor word jump for `TextInput`. [#108] +- `keyboard::ModifiersState` struct which contains the state of the keyboard modifiers. [#108] +- `TextInput::password` method to enable secure password input mode. [#113] +- `Button::height` and `Button::min_height` methods to control the height of a button. + +### Changed +- `Image::new` takes an `Into<image::Handle>` now instead of an `Into<String>`. [#90] +- `Button::background` takes an `Into<Background>` now instead of a `Background`. +- `keyboard::Event::Input` now contains key modifiers state. [#108] + +### Fixed +- `Image` widget not keeping aspect ratio consistently. [#90] +- `TextInput` not taking grapheme clusters into account. [#108] + +[#90]: https://github.com/hecrj/iced/pull/90 +[#108]: https://github.com/hecrj/iced/pull/108 +[#113]: https://github.com/hecrj/iced/pull/113 + ## [0.1.0] - 2019-11-25 ### Added diff --git a/native/Cargo.toml b/native/Cargo.toml index 7993676e..6276535e 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,6 +8,15 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command"] } twox-hash = "1.5" raw-window-handle = "0.3" +unicode-segmentation = "1.6" + +[dependencies.iced_core] +version = "0.1.0" +path = "../core" + +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" +features = ["thread-pool"] diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs new file mode 100644 index 00000000..4c574590 --- /dev/null +++ b/native/src/clipboard.rs @@ -0,0 +1,8 @@ +/// A buffer for short-term storage and transfer within and between +/// applications. +pub trait Clipboard { + /// Returns the current content of the [`Clipboard`] as text. + /// + /// [`Clipboard`]: trait.Clipboard.html + fn content(&self) -> Option<String>; +} diff --git a/native/src/element.rs b/native/src/element.rs index d4237fd0..276f7614 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,5 +1,5 @@ use crate::{ - layout, renderer, Color, Event, Hasher, Layout, Length, Point, Widget, + layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget, }; /// A generic [`Widget`]. @@ -171,7 +171,7 @@ where /// ``` pub fn map<F, B>(self, f: F) -> Element<'a, B, Renderer> where - Message: 'static + Clone, + Message: 'static, Renderer: 'a, B: 'static, F: 'static + Fn(Message) -> B, @@ -194,7 +194,7 @@ where ) -> Element<'a, Message, Renderer> where Message: 'static, - Renderer: 'a + renderer::Debugger, + Renderer: 'a + layout::Debugger, { Element { widget: Box::new(Explain::new(self, color.into())), @@ -234,13 +234,18 @@ where pub fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - self.widget.draw(renderer, layout, cursor_position) + self.widget + .draw(renderer, defaults, layout, cursor_position) } - pub(crate) fn hash_layout(&self, state: &mut Hasher) { + /// Computes the _layout_ hash of the [`Element`]. + /// + /// [`Element`]: struct.Element.html + pub fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } } @@ -267,7 +272,6 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> where - A: Clone, Renderer: crate::Renderer, { fn width(&self) -> Length { @@ -293,6 +297,7 @@ where cursor_position: Point, messages: &mut Vec<B>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { let mut original_messages = Vec::new(); @@ -302,21 +307,23 @@ where cursor_position, &mut original_messages, renderer, + clipboard, ); original_messages - .iter() - .cloned() + .drain(..) .for_each(|message| messages.push((self.mapper)(message))); } fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - self.widget.draw(renderer, layout, cursor_position) + self.widget + .draw(renderer, defaults, layout, cursor_position) } fn hash_layout(&self, state: &mut Hasher) { @@ -341,7 +348,7 @@ where impl<'a, Message, Renderer> Widget<Message, Renderer> for Explain<'a, Message, Renderer> where - Renderer: crate::Renderer + renderer::Debugger, + Renderer: crate::Renderer + layout::Debugger, { fn width(&self) -> Length { self.element.widget.width() @@ -366,6 +373,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { self.element.widget.on_event( event, @@ -373,16 +381,19 @@ where cursor_position, messages, renderer, + clipboard, ) } fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { renderer.explain( + defaults, self.element.widget.as_ref(), layout, cursor_position, diff --git a/native/src/event.rs b/native/src/event.rs index 71f06006..b2550ead 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,4 +1,7 @@ -use crate::input::{keyboard, mouse}; +use crate::{ + input::{keyboard, mouse}, + window, +}; /// A user interface event. /// @@ -6,11 +9,14 @@ use crate::input::{keyboard, mouse}; /// additional events, feel free to [open an issue] and share your use case!_ /// /// [open an issue]: https://github.com/hecrj/iced/issues -#[derive(PartialEq, Clone, Copy, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum Event { /// A keyboard event Keyboard(keyboard::Event), /// A mouse event Mouse(mouse::Event), + + /// A window event + Window(window::Event), } diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs index 57c24484..432e75ba 100644 --- a/native/src/input/keyboard.rs +++ b/native/src/input/keyboard.rs @@ -1,6 +1,8 @@ //! Build keyboard events. mod event; mod key_code; +mod modifiers_state; pub use event::Event; pub use key_code::KeyCode; +pub use modifiers_state::ModifiersState; diff --git a/native/src/input/keyboard/event.rs b/native/src/input/keyboard/event.rs index 8118f112..862f30c4 100644 --- a/native/src/input/keyboard/event.rs +++ b/native/src/input/keyboard/event.rs @@ -1,13 +1,13 @@ -use super::KeyCode; +use super::{KeyCode, ModifiersState}; use crate::input::ButtonState; -#[derive(Debug, Clone, Copy, PartialEq)] /// A keyboard event. /// /// _**Note:** This type is largely incomplete! If you need to track /// additional events, feel free to [open an issue] and share your use case!_ /// /// [open an issue]: https://github.com/hecrj/iced/issues +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Event { /// A keyboard key was pressed or released. Input { @@ -16,6 +16,9 @@ pub enum Event { /// The key identifier key_code: KeyCode, + + /// The state of the modifier keys + modifiers: ModifiersState, }, /// A unicode character was received. diff --git a/native/src/input/keyboard/modifiers_state.rs b/native/src/input/keyboard/modifiers_state.rs new file mode 100644 index 00000000..4e3794b3 --- /dev/null +++ b/native/src/input/keyboard/modifiers_state.rs @@ -0,0 +1,15 @@ +/// The current state of the keyboard modifiers. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ModifiersState { + /// Whether a shift key is pressed + pub shift: bool, + + /// Whether a control key is pressed + pub control: bool, + + /// Whether an alt key is pressed + pub alt: bool, + + /// Whether a logo key is pressed (e.g. windows key, command key...) + pub logo: bool, +} diff --git a/native/src/layout.rs b/native/src/layout.rs index e945706b..4a3ab94a 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -1,9 +1,11 @@ //! Position your widgets properly. +mod debugger; mod limits; mod node; pub mod flex; +pub use debugger::Debugger; pub use limits::Limits; pub use node::Node; diff --git a/native/src/renderer/debugger.rs b/native/src/layout/debugger.rs index 4cc50661..e4b21609 100644 --- a/native/src/renderer/debugger.rs +++ b/native/src/layout/debugger.rs @@ -1,9 +1,9 @@ -use crate::{Color, Layout, Point, Widget}; +use crate::{Color, Layout, Point, Renderer, Widget}; /// A renderer able to graphically explain a [`Layout`]. /// -/// [`Layout`]: ../struct.Layout.html -pub trait Debugger: super::Renderer { +/// [`Layout`]: struct.Layout.html +pub trait Debugger: Renderer { /// Explains the [`Layout`] of an [`Element`] for debugging purposes. /// /// This will be called when [`Element::explain`] has been used. It should @@ -13,10 +13,11 @@ pub trait Debugger: super::Renderer { /// [`Layout`] and its children. /// /// [`Layout`]: struct.Layout.html - /// [`Element`]: struct.Element.html - /// [`Element::explain`]: struct.Element.html#method.explain + /// [`Element`]: ../struct.Element.html + /// [`Element::explain`]: ../struct.Element.html#method.explain fn explain<Message>( &mut self, + defaults: &Self::Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index bc90553e..2f65f1c1 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -18,7 +18,7 @@ // limitations under the License. use crate::{ layout::{Limits, Node}, - Align, Element, Size, + Align, Element, Point, Size, }; /// The main axis of a flex layout. @@ -73,11 +73,12 @@ where Renderer: crate::Renderer, { let limits = limits.pad(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + let max_cross = axis.cross(limits.max()); - let mut total_non_fill = - spacing as f32 * (items.len() as i32 - 1).max(0) as f32; let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()); + let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); nodes.resize(items.len(), Node::default()); @@ -90,12 +91,15 @@ where .fill_factor(); if fill_factor == 0 { - let child_limits = Limits::new(Size::ZERO, limits.max()); + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); let layout = child.layout(renderer, &child_limits); let size = layout.size(); - total_non_fill += axis.main(size); + available -= axis.main(size); cross = cross.max(axis.cross(size)); nodes[i] = layout; @@ -104,8 +108,7 @@ where } } - let available = axis.main(limits.max()); - let remaining = (available - total_non_fill).max(0.0); + let remaining = available.max(0.0); for (i, child) in items.iter().enumerate() { let fill_factor = match axis { @@ -149,8 +152,7 @@ where let (x, y) = axis.pack(main, padding); - node.bounds.x = x; - node.bounds.y = y; + node.move_to(Point::new(x, y)); match axis { Axis::Horizontal => { @@ -166,13 +168,11 @@ where main += axis.main(size); } - let (width, height) = axis.pack(main, cross); + let (width, height) = axis.pack(main - padding, cross); let size = limits.resolve(Size::new(width, height)); - let (padding_x, padding_y) = axis.pack(padding, padding * 2.0); - Node::with_children( - Size::new(size.width + padding_x, size.height + padding_y), + Size::new(size.width + padding * 2.0, size.height + padding * 2.0), nodes, ) } diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index 740417d4..664c881a 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -44,6 +44,14 @@ impl Limits { self.max } + /// Returns the fill [`Size`] of the [`Limits`]. + /// + /// [`Limits`]: struct.Limits.html + /// [`Size`]: ../struct.Size.html + pub fn fill(&self) -> Size { + self.fill + } + /// Applies a width constraint to the current [`Limits`]. /// /// [`Limits`]: struct.Limits.html @@ -52,7 +60,7 @@ impl Limits { Length::Shrink => { self.fill.width = self.min.width; } - Length::Fill => { + Length::Fill | Length::FillPortion(_) => { self.fill.width = self.fill.width.min(self.max.width); } Length::Units(units) => { @@ -76,7 +84,7 @@ impl Limits { Length::Shrink => { self.fill.height = self.min.height; } - Length::Fill => { + Length::Fill | Length::FillPortion(_) => { self.fill.height = self.fill.height.min(self.max.height); } Length::Units(units) => { diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs index acfd33bd..a265c46a 100644 --- a/native/src/layout/node.rs +++ b/native/src/layout/node.rs @@ -1,9 +1,9 @@ -use crate::{Align, Rectangle, Size}; +use crate::{Align, Point, Rectangle, Size}; /// The bounds of an element and its children. #[derive(Debug, Clone, Default)] pub struct Node { - pub(crate) bounds: Rectangle, + bounds: Rectangle, children: Vec<Node>, } @@ -54,7 +54,10 @@ impl Node { &self.children } - pub(crate) fn align( + /// Aligns the [`Node`] in the given space. + /// + /// [`Node`]: struct.Node.html + pub fn align( &mut self, horizontal_alignment: Align, vertical_alignment: Align, @@ -80,4 +83,12 @@ impl Node { } } } + + /// Moves the [`Node`] to the given position. + /// + /// [`Node`]: struct.Node.html + pub fn move_to(&mut self, position: Point) { + self.bounds.x = position.x; + self.bounds.y = position.y; + } } diff --git a/native/src/lib.rs b/native/src/lib.rs index 45c3c699..e4e7baee 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -14,7 +14,7 @@ //! - A [`Widget`] trait, which is used to implement new widgets: from layout //! requirements to event and drawing logic. //! - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. -//! - A [`Windowed`] trait, leveraging [`raw-window-handle`], which can be +//! - A [`window::Renderer`] trait, leveraging [`raw-window-handle`], which can be //! implemented by graphical renderers that target _windows_. Window-based //! shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic. //! @@ -31,37 +31,46 @@ //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [`Widget`]: widget/trait.Widget.html -//! [`Windowed`]: renderer/trait.Windowed.html +//! [`window::Renderer`]: window/trait.Renderer.html //! [`UserInterface`]: struct.UserInterface.html //! [renderer]: renderer/index.html #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] pub mod input; pub mod layout; pub mod renderer; +pub mod subscription; pub mod widget; +pub mod window; +mod clipboard; mod element; mod event; mod hasher; mod mouse_cursor; -mod size; +mod runtime; mod user_interface; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Point, Rectangle, Vector, VerticalAlignment, + Align, Background, Color, Font, HorizontalAlignment, Length, Point, + Rectangle, Size, Vector, VerticalAlignment, }; +pub use iced_futures::{executor, futures, Command}; +#[doc(no_inline)] +pub use executor::Executor; + +pub use clipboard::Clipboard; pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; pub use mouse_cursor::MouseCursor; pub use renderer::Renderer; -pub use size::Size; +pub use runtime::Runtime; +pub use subscription::Subscription; pub use user_interface::{Cache, UserInterface}; pub use widget::*; diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs index a4740a27..c7297e0e 100644 --- a/native/src/mouse_cursor.rs +++ b/native/src/mouse_cursor.rs @@ -22,3 +22,9 @@ pub enum MouseCursor { /// The cursor is over a text widget. Text, } + +impl Default for MouseCursor { + fn default() -> MouseCursor { + MouseCursor::OutOfBounds + } +} diff --git a/native/src/renderer.rs b/native/src/renderer.rs index 7a68ada4..a16df72b 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -20,15 +20,10 @@ //! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html //! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html -mod debugger; #[cfg(debug_assertions)] mod null; -mod windowed; - -pub use debugger::Debugger; #[cfg(debug_assertions)] pub use null::Null; -pub use windowed::{Target, Windowed}; use crate::{layout, Element}; @@ -43,6 +38,13 @@ pub trait Renderer: Sized { /// [`Renderer`]: trait.Renderer.html type Output; + /// The default styling attributes of the [`Renderer`]. + /// + /// This type can be leveraged to implement style inheritance. + /// + /// [`Renderer`]: trait.Renderer.html + type Defaults: Default; + /// Lays out the elements of a user interface. /// /// You should override this if you need to perform any operations before or @@ -50,7 +52,8 @@ pub trait Renderer: Sized { fn layout<'a, Message>( &mut self, element: &Element<'a, Message, Self>, + limits: &layout::Limits, ) -> layout::Node { - element.layout(self, &layout::Limits::NONE) + element.layout(self, limits) } } diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 182f033a..0fcce5ad 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,20 +1,33 @@ use crate::{ - button, checkbox, column, radio, row, scrollable, text, text_input, - Background, Color, Element, Font, HorizontalAlignment, Layout, Point, + button, checkbox, column, progress_bar, radio, row, scrollable, slider, + text, text_input, Color, Element, Font, HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size, VerticalAlignment, }; /// A renderer that does nothing. +/// +/// It can be useful if you are writing tests! #[derive(Debug, Clone, Copy)] pub struct Null; +impl Null { + /// Creates a new [`Null`] renderer. + /// + /// [`Null`]: struct.Null.html + pub fn new() -> Null { + Null + } +} + impl Renderer for Null { type Output = (); + type Defaults = (); } impl column::Renderer for Null { fn draw<Message>( &mut self, + _defaults: &Self::Defaults, _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, @@ -25,6 +38,7 @@ impl column::Renderer for Null { impl row::Renderer for Null { fn draw<Message>( &mut self, + _defaults: &Self::Defaults, _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, @@ -33,9 +47,7 @@ impl row::Renderer for Null { } impl text::Renderer for Null { - fn default_size(&self) -> u16 { - 20 - } + const DEFAULT_SIZE: u16 = 20; fn measure( &self, @@ -49,6 +61,7 @@ impl text::Renderer for Null { fn draw( &mut self, + _defaults: &Self::Defaults, _bounds: Rectangle, _content: &str, _size: u16, @@ -61,13 +74,15 @@ impl text::Renderer for Null { } impl scrollable::Renderer for Null { - fn is_mouse_over_scrollbar( + type Style = (); + + fn scrollbar( &self, _bounds: Rectangle, _content_bounds: Rectangle, - _cursor_position: Point, - ) -> bool { - false + _offset: u32, + ) -> Option<scrollable::Scrollbar> { + None } fn draw( @@ -77,44 +92,73 @@ impl scrollable::Renderer for Null { _content_bounds: Rectangle, _is_mouse_over: bool, _is_mouse_over_scrollbar: bool, + _scrollbar: Option<scrollable::Scrollbar>, _offset: u32, + _style: &Self::Style, _content: Self::Output, ) { } } impl text_input::Renderer for Null { + type Style = (); + fn default_size(&self) -> u16 { 20 } + fn measure_value(&self, _value: &str, _size: u16, _font: Font) -> f32 { + 0.0 + } + + fn offset( + &self, + _text_bounds: Rectangle, + _size: u16, + _value: &text_input::Value, + _state: &text_input::State, + _font: Font, + ) -> f32 { + 0.0 + } + fn draw( &mut self, _bounds: Rectangle, _text_bounds: Rectangle, _cursor_position: Point, _size: u16, + _font: Font, _placeholder: &str, _value: &text_input::Value, _state: &text_input::State, + _style: &Self::Style, ) -> Self::Output { } } impl button::Renderer for Null { - fn draw( + const DEFAULT_PADDING: u16 = 0; + + type Style = (); + + fn draw<Message>( &mut self, + _defaults: &Self::Defaults, _bounds: Rectangle, _cursor_position: Point, + _is_disabled: bool, _is_pressed: bool, - _background: Option<Background>, - _border_radius: u16, - _content: Self::Output, + _style: &Self::Style, + _content: &Element<'_, Message, Self>, + _content_layout: Layout<'_>, ) -> Self::Output { } } impl radio::Renderer for Null { + type Style = (); + fn default_size(&self) -> u32 { 20 } @@ -125,14 +169,16 @@ impl radio::Renderer for Null { _is_selected: bool, _is_mouse_over: bool, _label: Self::Output, + _style: &Self::Style, ) { } } impl checkbox::Renderer for Null { - fn default_size(&self) -> u32 { - 20 - } + type Style = (); + + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, @@ -140,6 +186,41 @@ impl checkbox::Renderer for Null { _is_checked: bool, _is_mouse_over: bool, _label: Self::Output, + _style: &Self::Style, + ) { + } +} + +impl slider::Renderer for Null { + type Style = (); + + fn height(&self) -> u32 { + 30 + } + + fn draw( + &mut self, + _bounds: Rectangle, + _cursor_position: Point, + _range: std::ops::RangeInclusive<f32>, + _value: f32, + _is_dragging: bool, + _style_sheet: &Self::Style, + ) { + } +} + +impl progress_bar::Renderer for Null { + type Style = (); + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + _bounds: Rectangle, + _range: std::ops::RangeInclusive<f32>, + _value: f32, + _style: &Self::Style, ) { } } diff --git a/native/src/renderer/windowed.rs b/native/src/renderer/windowed.rs deleted file mode 100644 index 813a03f2..00000000 --- a/native/src/renderer/windowed.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::MouseCursor; - -use raw_window_handle::HasRawWindowHandle; - -/// A renderer that can target windows. -pub trait Windowed: super::Renderer + Sized { - /// The type of target. - type Target: Target<Renderer = Self>; - - /// Creates a new [`Windowed`] renderer. - /// - /// [`Windowed`]: trait.Windowed.html - fn new() -> Self; - - /// Performs the drawing operations described in the output on the given - /// target. - /// - /// The overlay can be a bunch of debug text logs. It should be rendered on - /// top of the GUI on most scenarios. - fn draw<T: AsRef<str>>( - &mut self, - output: &Self::Output, - overlay: &[T], - target: &mut Self::Target, - ) -> MouseCursor; -} - -/// A rendering target. -pub trait Target { - /// The renderer of this target. - type Renderer; - - /// Creates a new rendering [`Target`] from the given window handle, width, - /// height and dpi factor. - /// - /// [`Target`]: trait.Target.html - fn new<W: HasRawWindowHandle>( - window: &W, - width: u16, - height: u16, - dpi: f32, - renderer: &Self::Renderer, - ) -> Self; - - /// Resizes the current [`Target`]. - /// - /// [`Target`]: trait.Target.html - fn resize( - &mut self, - width: u16, - height: u16, - dpi: f32, - renderer: &Self::Renderer, - ); -} diff --git a/native/src/runtime.rs b/native/src/runtime.rs new file mode 100644 index 00000000..9fa031f4 --- /dev/null +++ b/native/src/runtime.rs @@ -0,0 +1,12 @@ +//! Run commands and subscriptions. +use crate::{Event, Hasher}; + +/// A native runtime with a generic executor and receiver of results. +/// +/// It can be used by shells to easily spawn a [`Command`] or track a +/// [`Subscription`]. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: ../struct.Subscription.html +pub type Runtime<Executor, Receiver, Message> = + iced_futures::Runtime<Hasher, Event, Executor, Receiver, Message>; diff --git a/native/src/subscription.rs b/native/src/subscription.rs new file mode 100644 index 00000000..0d002c6c --- /dev/null +++ b/native/src/subscription.rs @@ -0,0 +1,47 @@ +//! Listen to external events in your application. +use crate::{Event, Hasher}; +use iced_futures::futures::stream::BoxStream; + +/// A request to listen to external events. +/// +/// Besides performing async actions on demand with [`Command`], most +/// applications also need to listen to external events passively. +/// +/// A [`Subscription`] is normally provided to some runtime, like a [`Command`], +/// and it will generate events as long as the user keeps requesting it. +/// +/// For instance, you can use a [`Subscription`] to listen to a WebSocket +/// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: struct.Subscription.html +pub type Subscription<T> = iced_futures::Subscription<Hasher, Event, T>; + +/// A stream of runtime events. +/// +/// It is the input of a [`Subscription`] in the native runtime. +/// +/// [`Subscription`]: type.Subscription.html +pub type EventStream = BoxStream<'static, Event>; + +/// A native [`Subscription`] tracker. +/// +/// [`Subscription`]: type.Subscription.html +pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>; + +pub use iced_futures::subscription::Recipe; + +mod events; + +use events::Events; + +/// Returns a [`Subscription`] to all the runtime events. +/// +/// This subscription will notify your application of any [`Event`] handled by +/// the runtime. +/// +/// [`Subscription`]: type.Subscription.html +/// [`Event`]: ../enum.Event.html +pub fn events() -> Subscription<Event> { + Subscription::from_recipe(Events) +} diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs new file mode 100644 index 00000000..7d33166e --- /dev/null +++ b/native/src/subscription/events.rs @@ -0,0 +1,24 @@ +use crate::{ + subscription::{EventStream, Recipe}, + Event, Hasher, +}; +use iced_futures::futures::stream::BoxStream; + +pub struct Events; + +impl Recipe<Hasher, Event> for Events { + type Output = Event; + + fn hash(&self, state: &mut Hasher) { + use std::hash::Hash; + + std::any::TypeId::of::<Self>().hash(state); + } + + fn stream( + self: Box<Self>, + event_stream: EventStream, + ) -> BoxStream<'static, Self::Output> { + event_stream + } +} diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 9833c815..08914bed 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,6 @@ -use crate::{input::mouse, layout, Element, Event, Layout, Point, Size}; +use crate::{ + input::mouse, layout, Clipboard, Element, Event, Layout, Point, Size, +}; use std::hash::Hasher; @@ -15,6 +17,7 @@ pub struct UserInterface<'a, Message, Renderer> { hash: u64, root: Element<'a, Message, Renderer>, layout: layout::Node, + bounds: Size, cursor_position: Point, } @@ -37,28 +40,11 @@ where /// is naive way to set up our application loop: /// /// ```no_run - /// use iced_native::{UserInterface, Cache}; + /// use iced_native::{UserInterface, Cache, Size}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { - /// # pub struct Renderer; - /// # - /// # impl Renderer { - /// # pub fn new() -> Self { Renderer } - /// # } - /// # - /// # impl iced_native::Renderer for Renderer { type Output = (); } - /// # - /// # impl iced_native::column::Renderer for Renderer { - /// # fn draw<Message>( - /// # &mut self, - /// # _children: &[iced_native::Element<'_, Message, Self>], - /// # _layout: iced_native::Layout<'_>, - /// # _cursor_position: iced_native::Point, - /// # ) -> Self::Output { - /// # () - /// # } - /// # } + /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # /// # use iced_native::Column; @@ -75,6 +61,7 @@ where /// let mut counter = Counter::new(); /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); + /// let mut window_size = Size::new(1024.0, 768.0); /// /// // Application loop /// loop { @@ -83,6 +70,7 @@ where /// // Build the user interface /// let user_interface = UserInterface::build( /// counter.view(), + /// window_size, /// cache, /// &mut renderer, /// ); @@ -96,26 +84,30 @@ where /// ``` pub fn build<E: Into<Element<'a, Message, Renderer>>>( root: E, + bounds: Size, cache: Cache, renderer: &mut Renderer, ) -> Self { let root = root.into(); - let hasher = &mut crate::Hasher::default(); - root.hash_layout(hasher); + let hash = { + let hasher = &mut crate::Hasher::default(); + root.hash_layout(hasher); - let hash = hasher.finish(); + hasher.finish() + }; - let layout = if hash == cache.hash { + let layout = if hash == cache.hash && bounds == cache.bounds { cache.layout } else { - renderer.layout(&root) + renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) }; UserInterface { hash, root, layout, + bounds, cursor_position: cache.cursor_position, } } @@ -133,28 +125,11 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache}; + /// use iced_native::{UserInterface, Cache, Size}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { - /// # pub struct Renderer; - /// # - /// # impl Renderer { - /// # pub fn new() -> Self { Renderer } - /// # } - /// # - /// # impl iced_native::Renderer for Renderer { type Output = (); } - /// # - /// # impl iced_native::column::Renderer for Renderer { - /// # fn draw<Message>( - /// # &mut self, - /// # _children: &[iced_native::Element<'_, Message, Self>], - /// # _layout: iced_native::Layout<'_>, - /// # _cursor_position: iced_native::Point, - /// # ) -> Self::Output { - /// # () - /// # } - /// # } + /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # /// # use iced_native::Column; @@ -171,6 +146,7 @@ where /// let mut counter = Counter::new(); /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); + /// let mut window_size = Size::new(1024.0, 768.0); /// /// // Initialize our event storage /// let mut events = Vec::new(); @@ -180,12 +156,13 @@ where /// /// let mut user_interface = UserInterface::build( /// counter.view(), + /// window_size, /// cache, /// &mut renderer, /// ); /// /// // Update the user interface - /// let messages = user_interface.update(&renderer, events.drain(..)); + /// let messages = user_interface.update(events.drain(..), None, &renderer); /// /// cache = user_interface.into_cache(); /// @@ -197,8 +174,9 @@ where /// ``` pub fn update( &mut self, + events: impl IntoIterator<Item = Event>, + clipboard: Option<&dyn Clipboard>, renderer: &Renderer, - events: impl Iterator<Item = Event>, ) -> Vec<Message> { let mut messages = Vec::new(); @@ -213,6 +191,7 @@ where self.cursor_position, &mut messages, renderer, + clipboard, ); } @@ -233,28 +212,11 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache}; + /// use iced_native::{UserInterface, Cache, Size}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { - /// # pub struct Renderer; - /// # - /// # impl Renderer { - /// # pub fn new() -> Self { Renderer } - /// # } - /// # - /// # impl iced_native::Renderer for Renderer { type Output = (); } - /// # - /// # impl iced_native::column::Renderer for Renderer { - /// # fn draw<Message>( - /// # &mut self, - /// # _children: &[iced_native::Element<'_, Message, Self>], - /// # _layout: iced_native::Layout<'_>, - /// # _cursor_position: iced_native::Point, - /// # ) -> Self::Output { - /// # () - /// # } - /// # } + /// # pub use iced_native::renderer::Null as Renderer; /// # } /// # /// # use iced_native::Column; @@ -271,6 +233,7 @@ where /// let mut counter = Counter::new(); /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); + /// let mut window_size = Size::new(1024.0, 768.0); /// let mut events = Vec::new(); /// /// loop { @@ -278,11 +241,12 @@ where /// /// let mut user_interface = UserInterface::build( /// counter.view(), + /// window_size, /// cache, /// &mut renderer, /// ); /// - /// let messages = user_interface.update(&renderer, events.drain(..)); + /// let messages = user_interface.update(events.drain(..), None, &renderer); /// /// // Draw the user interface /// let mouse_cursor = user_interface.draw(&mut renderer); @@ -300,6 +264,7 @@ where pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output { self.root.widget.draw( renderer, + &Renderer::Defaults::default(), Layout::new(&self.layout), self.cursor_position, ) @@ -314,6 +279,7 @@ where Cache { hash: self.hash, layout: self.layout, + bounds: self.bounds, cursor_position: self.cursor_position, } } @@ -326,6 +292,7 @@ where pub struct Cache { hash: u64, layout: layout::Node, + bounds: Size, cursor_position: Point, } @@ -341,6 +308,7 @@ impl Cache { Cache { hash: 0, layout: layout::Node::new(Size::new(0.0, 0.0)), + bounds: Size::ZERO, cursor_position: Point::new(-1.0, -1.0), } } diff --git a/native/src/widget.rs b/native/src/widget.rs index 71dcdc0d..f9424b02 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -25,10 +25,13 @@ pub mod checkbox; pub mod column; pub mod container; pub mod image; +pub mod progress_bar; pub mod radio; pub mod row; pub mod scrollable; pub mod slider; +pub mod space; +pub mod svg; pub mod text; pub mod text_input; @@ -43,6 +46,8 @@ pub use container::Container; #[doc(no_inline)] pub use image::Image; #[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] pub use radio::Radio; #[doc(no_inline)] pub use row::Row; @@ -51,11 +56,15 @@ pub use scrollable::Scrollable; #[doc(no_inline)] pub use slider::Slider; #[doc(no_inline)] +pub use space::Space; +#[doc(no_inline)] +pub use svg::Svg; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; -use crate::{layout, Event, Hasher, Layout, Length, Point}; +use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; /// A component that displays information and allows interaction. /// @@ -98,6 +107,7 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output; @@ -139,6 +149,7 @@ where _cursor_position: Point, _messages: &mut Vec<Message>, _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, ) { } } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 023c4ee8..f1d46936 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -6,7 +6,7 @@ //! [`State`]: struct.State.html use crate::{ input::{mouse, ButtonState}, - layout, Background, Element, Event, Hasher, Layout, Length, Point, + layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -28,18 +28,22 @@ use std::hash::Hash; /// .on_press(Message::ButtonPressed); /// ``` #[allow(missing_debug_implementations)] -pub struct Button<'a, Message, Renderer> { +pub struct Button<'a, Message, Renderer: self::Renderer> { state: &'a mut State, content: Element<'a, Message, Renderer>, on_press: Option<Message>, width: Length, + height: Length, min_width: u32, + min_height: u32, padding: u16, - background: Option<Background>, - border_radius: u16, + style: Renderer::Style, } -impl<'a, Message, Renderer> Button<'a, Message, Renderer> { +impl<'a, Message, Renderer> Button<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ /// Creates a new [`Button`] with some local [`State`] and the given /// content. /// @@ -54,10 +58,11 @@ impl<'a, Message, Renderer> Button<'a, Message, Renderer> { content: content.into(), on_press: None, width: Length::Shrink, + height: Length::Shrink, min_width: 0, - padding: 0, - background: None, - border_radius: 0, + min_height: 0, + padding: Renderer::DEFAULT_PADDING, + style: Renderer::Style::default(), } } @@ -69,36 +74,35 @@ impl<'a, Message, Renderer> Button<'a, Message, Renderer> { self } - /// Sets the minimum width of the [`Button`]. + /// Sets the height of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn min_width(mut self, min_width: u32) -> Self { - self.min_width = min_width; + pub fn height(mut self, height: Length) -> Self { + self.height = height; self } - /// Sets the padding of the [`Button`]. + /// Sets the minimum width of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + pub fn min_width(mut self, min_width: u32) -> Self { + self.min_width = min_width; self } - /// Sets the [`Background`] of the [`Button`]. + /// Sets the minimum height of the [`Button`]. /// /// [`Button`]: struct.Button.html - /// [`Background`]: ../../struct.Background.html - pub fn background(mut self, background: Background) -> Self { - self.background = Some(background); + pub fn min_height(mut self, min_height: u32) -> Self { + self.min_height = min_height; self } - /// Sets the border radius of the [`Button`]. + /// Sets the padding of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn border_radius(mut self, border_radius: u16) -> Self { - self.border_radius = border_radius; + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; self } @@ -109,6 +113,14 @@ impl<'a, Message, Renderer> Button<'a, Message, Renderer> { self.on_press = Some(msg); self } + + /// Sets the style of the [`Button`]. + /// + /// [`Button`]: struct.Button.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } } /// The local state of a [`Button`]. @@ -139,7 +151,7 @@ where } fn height(&self) -> Length { - Length::Shrink + self.height } fn layout( @@ -150,14 +162,13 @@ where let padding = f32::from(self.padding); let limits = limits .min_width(self.min_width) + .min_height(self.min_height) .width(self.width) - .height(Length::Shrink) + .height(self.height) .pad(padding); let mut content = self.content.layout(renderer, &limits); - - content.bounds.x = padding; - content.bounds.y = padding; + content.move_to(Point::new(padding, padding)); let size = limits.resolve(content.size()).pad(padding); @@ -171,6 +182,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, ) { match event { Event::Mouse(mouse::Event::Input { @@ -205,22 +217,19 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - let content = self.content.draw( - renderer, - layout.children().next().unwrap(), - cursor_position, - ); - renderer.draw( + defaults, layout.bounds(), cursor_position, + self.on_press.is_none(), self.state.is_pressed, - self.background, - self.border_radius, - content, + &self.style, + &self.content, + layout.children().next().unwrap(), ) } @@ -238,17 +247,27 @@ where /// [`Button`]: struct.Button.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { + /// The default padding of a [`Button`]. + /// + /// [`Button`]: struct.Button.html + const DEFAULT_PADDING: u16; + + /// The style supported by this renderer. + type Style: Default; + /// Draws a [`Button`]. /// /// [`Button`]: struct.Button.html - fn draw( + fn draw<Message>( &mut self, + defaults: &Self::Defaults, bounds: Rectangle, cursor_position: Point, + is_disabled: bool, is_pressed: bool, - background: Option<Background>, - border_radius: u16, - content: Self::Output, + style: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, ) -> Self::Output; } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 9563291c..b36d10a4 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -3,7 +3,7 @@ use std::hash::Hash; use crate::{ input::{mouse, ButtonState}, - layout, row, text, Align, Color, Element, Event, Font, Hasher, + layout, row, text, Align, Clipboard, Element, Event, Font, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -13,7 +13,7 @@ use crate::{ /// # Example /// /// ``` -/// # use iced_native::Checkbox; +/// # type Checkbox<Message> = iced_native::Checkbox<Message, iced_native::renderer::Null>; /// # /// pub enum Message { /// CheckboxToggled(bool), @@ -26,14 +26,20 @@ use crate::{ /// ///  #[allow(missing_debug_implementations)] -pub struct Checkbox<Message> { +pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { is_checked: bool, on_toggle: Box<dyn Fn(bool) -> Message>, label: String, - label_color: Option<Color>, + width: Length, + size: u16, + spacing: u16, + text_size: u16, + style: Renderer::Style, } -impl<Message> Checkbox<Message> { +impl<Message, Renderer: self::Renderer + text::Renderer> + Checkbox<Message, Renderer> +{ /// Creates a new [`Checkbox`]. /// /// It expects: @@ -52,25 +58,62 @@ impl<Message> Checkbox<Message> { is_checked, on_toggle: Box::new(f), label: String::from(label), - label_color: None, + width: Length::Shrink, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + spacing: Renderer::DEFAULT_SPACING, + text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + style: Renderer::Style::default(), } } - /// Sets the color of the label of the [`Checkbox`]. + /// Sets the size of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Checkbox`]. /// /// [`Checkbox`]: struct.Checkbox.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the spacing between the [`Checkbox`] and the text. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the text size of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = text_size; + self + } + + /// Sets the style of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for Checkbox<Message> +impl<Message, Renderer> Widget<Message, Renderer> + for Checkbox<Message, Renderer> where Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { - Length::Fill + self.width } fn height(&self) -> Length { @@ -82,17 +125,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = self::Renderer::default_size(renderer); - Row::<(), Renderer>::new() - .spacing(15) + .width(self.width) + .spacing(self.spacing) .align_items(Align::Center) .push( Row::new() - .width(Length::Units(size as u16)) - .height(Length::Units(size as u16)), + .width(Length::Units(self.size)) + .height(Length::Units(self.size)), + ) + .push( + Text::new(&self.label) + .width(self.width) + .size(self.text_size), ) - .push(Text::new(&self.label)) .layout(renderer, limits) } @@ -103,6 +149,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, ) { match event { Event::Mouse(mouse::Event::Input { @@ -122,6 +169,7 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { @@ -134,11 +182,12 @@ where let label = text::Renderer::draw( renderer, + defaults, label_layout.bounds(), &self.label, - text::Renderer::default_size(renderer), + self.text_size, Font::Default, - self.label_color, + None, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -151,6 +200,7 @@ where self.is_checked, is_mouse_over, label, + &self.style, ) } @@ -167,10 +217,18 @@ where /// [`Checkbox`]: struct.Checkbox.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer { - /// Returns the default size of a [`Checkbox`]. + /// The style supported by this renderer. + type Style: Default; + + /// The default size of a [`Checkbox`]. /// /// [`Checkbox`]: struct.Checkbox.html - fn default_size(&self) -> u32; + const DEFAULT_SIZE: u16; + + /// The default spacing of a [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + const DEFAULT_SPACING: u16; /// Draws a [`Checkbox`]. /// @@ -187,16 +245,19 @@ pub trait Renderer: crate::Renderer { is_checked: bool, is_mouse_over: bool, label: Self::Output, + style: &Self::Style, ) -> Self::Output; } -impl<'a, Message, Renderer> From<Checkbox<Message>> +impl<'a, Message, Renderer> From<Checkbox<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, + Renderer: 'static + self::Renderer + text::Renderer + row::Renderer, Message: 'static, { - fn from(checkbox: Checkbox<Message>) -> Element<'a, Message, Renderer> { + fn from( + checkbox: Checkbox<Message, Renderer>, + ) -> Element<'a, Message, Renderer> { Element::new(checkbox) } } diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index cdcf25af..104790d4 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -2,7 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Element, Event, Hasher, Layout, Length, Point, Widget, + layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Widget, }; use std::u32; @@ -32,7 +33,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { Column { spacing: 0, padding: 0, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, max_height: u32::MAX, @@ -153,15 +154,17 @@ where cursor_position: Point, messages: &mut Vec<Message>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { self.children.iter_mut().zip(layout.children()).for_each( |(child, layout)| { child.widget.on_event( - event, + event.clone(), layout, cursor_position, messages, renderer, + clipboard, ) }, ); @@ -170,10 +173,11 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - renderer.draw(&self.children, layout, cursor_position) + renderer.draw(defaults, &self.children, layout, cursor_position) } fn hash_layout(&self, state: &mut Hasher) { @@ -210,6 +214,7 @@ pub trait Renderer: crate::Renderer + Sized { /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, + defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 7852eecf..3459a832 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -2,7 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Element, Event, Hasher, Layout, Length, Point, Widget, + layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Rectangle, Widget, }; use std::u32; @@ -11,17 +12,21 @@ use std::u32; /// /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer> { +pub struct Container<'a, Message, Renderer: self::Renderer> { width: Length, height: Length, max_width: u32, max_height: u32, horizontal_alignment: Align, vertical_alignment: Align, + style: Renderer::Style, content: Element<'a, Message, Renderer>, } -impl<'a, Message, Renderer> Container<'a, Message, Renderer> { +impl<'a, Message, Renderer> Container<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ /// Creates an empty [`Container`]. /// /// [`Container`]: struct.Container.html @@ -36,6 +41,7 @@ impl<'a, Message, Renderer> Container<'a, Message, Renderer> { max_height: u32::MAX, horizontal_alignment: Align::Start, vertical_alignment: Align::Start, + style: Renderer::Style::default(), content: content.into(), } } @@ -71,13 +77,28 @@ impl<'a, Message, Renderer> Container<'a, Message, Renderer> { self.max_height = max_height; self } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + /// + /// [`Container`]: struct.Container.html + pub fn align_x(mut self, alignment: Align) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + /// + /// [`Container`]: struct.Container.html + pub fn align_y(mut self, alignment: Align) -> Self { + self.vertical_alignment = alignment; + self + } /// Centers the contents in the horizontal axis of the [`Container`]. /// /// [`Container`]: struct.Container.html pub fn center_x(mut self) -> Self { self.horizontal_alignment = Align::Center; - self } @@ -86,7 +107,14 @@ impl<'a, Message, Renderer> Container<'a, Message, Renderer> { /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; + self + } + /// Sets the style of the [`Container`]. + /// + /// [`Container`]: struct.Container.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); self } } @@ -94,7 +122,7 @@ impl<'a, Message, Renderer> Container<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where - Renderer: crate::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -131,6 +159,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { self.content.widget.on_event( event, @@ -138,19 +167,24 @@ where cursor_position, messages, renderer, + clipboard, ) } fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - self.content.draw( - renderer, - layout.children().next().unwrap(), + renderer.draw( + defaults, + layout.bounds(), cursor_position, + &self.style, + &self.content, + layout.children().next().unwrap(), ) } @@ -165,10 +199,35 @@ where } } +/// The renderer of a [`Container`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Container`] in your user interface. +/// +/// [`Container`]: struct.Container.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// Draws a [`Container`]. + /// + /// [`Container`]: struct.Container.html + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + cursor_position: Point, + style: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output; +} + impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, + Renderer: 'a + self::Renderer, Message: 'static, { fn from( diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 4c588c9d..fbe38bfc 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,8 +1,11 @@ //! Display images in your user interface. - use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; -use std::hash::Hash; +use std::{ + hash::{Hash, Hasher as _}, + path::PathBuf, + sync::Arc, +}; /// A frame that displays an image while keeping aspect ratio. /// @@ -15,9 +18,9 @@ use std::hash::Hash; /// ``` /// /// <img src="https://github.com/hecrj/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300"> -#[derive(Debug)] +#[derive(Debug, Hash)] pub struct Image { - path: String, + handle: Handle, width: Length, height: Length, } @@ -26,9 +29,9 @@ impl Image { /// Creates a new [`Image`] with the given path. /// /// [`Image`]: struct.Image.html - pub fn new<T: Into<String>>(path: T) -> Self { + pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { - path: path.into(), + handle: handle.into(), width: Length::Shrink, height: Length::Shrink, } @@ -68,24 +71,22 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let (width, height) = renderer.dimensions(&self.path); + let (width, height) = renderer.dimensions(&self.handle); let aspect_ratio = width as f32 / height as f32; - // TODO: Deal with additional cases - let (width, height) = match (self.width, self.height) { - (Length::Units(width), _) => ( - self.width, - Length::Units((width as f32 / aspect_ratio).round() as u16), - ), - (_, _) => { - (Length::Units(width as u16), Length::Units(height as u16)) - } - }; + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(width as f32, height as f32)); - let mut size = limits.width(width).height(height).resolve(Size::ZERO); + let viewport_aspect_ratio = size.width / size.height; - size.height = size.width / aspect_ratio; + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } layout::Node::new(size) } @@ -93,18 +94,139 @@ where fn draw( &self, renderer: &mut Renderer, + _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, ) -> Renderer::Output { - renderer.draw(&self.path, layout) + renderer.draw(self.handle.clone(), layout) } fn hash_layout(&self, state: &mut Hasher) { + self.handle.hash(state); self.width.hash(state); self.height.hash(state); } } +/// An [`Image`] handle. +/// +/// [`Image`]: struct.Image.html +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc<Data>, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + /// Creates an image [`Handle`] containing the image pixels directly. This + /// function expects the input data to be provided as a `Vec<u8>` of BGRA + /// pixels. + /// + /// This is useful if you have already decoded your image. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle { + Self::from_data(Data::Pixels { + width, + height, + pixels, + }) + } + + /// Creates an image [`Handle`] containing the image data directly. + /// + /// This is useful if you already have your image loaded in-memory, maybe + /// because you downloaded or generated it procedurally. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_memory(bytes: Vec<u8>) -> Handle { + Self::from_data(Data::Bytes(bytes)) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data: Arc::new(data), + } + } + + /// Returns the unique identifier of the [`Handle`]. + /// + /// [`Handle`]: struct.Handle.html + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the image [`Data`]. + /// + /// [`Data`]: enum.Data.html + pub fn data(&self) -> &Data { + &self.data + } +} + +impl From<String> for Handle { + fn from(path: String) -> Handle { + Handle::from_path(path) + } +} + +impl From<&str> for Handle { + fn from(path: &str) -> Handle { + Handle::from_path(path) + } +} + +impl Hash for Handle { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// The data of an [`Image`]. +/// +/// [`Image`]: struct.Image.html +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + Bytes(Vec<u8>), + + /// Decoded image pixels in BGRA format. + Pixels { + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Vec<u8>, + }, +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({:?})", path), + Data::Bytes(_) => write!(f, "Bytes(...)"), + Data::Pixels { width, height, .. } => { + write!(f, "Pixels({} * {})", width, height) + } + } + } +} + /// The renderer of an [`Image`]. /// /// Your [renderer] will need to implement this trait before being able to use @@ -116,12 +238,12 @@ pub trait Renderer: crate::Renderer { /// Returns the dimensions of an [`Image`] located on the given path. /// /// [`Image`]: struct.Image.html - fn dimensions(&self, path: &str) -> (u32, u32); + fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Image`]. /// /// [`Image`]: struct.Image.html - fn draw(&mut self, path: &str, layout: Layout<'_>) -> Self::Output; + fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } impl<'a, Message, Renderer> From<Image> for Element<'a, Message, Renderer> diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs new file mode 100644 index 00000000..67d1ab83 --- /dev/null +++ b/native/src/widget/progress_bar.rs @@ -0,0 +1,168 @@ +//! Provide progress feedback to your users. +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; + +use std::{hash::Hash, ops::RangeInclusive}; + +/// A bar that displays progress. +/// +/// # Example +/// ``` +/// # use iced_native::renderer::Null; +/// # +/// # pub type ProgressBar = iced_native::ProgressBar<Null>; +/// let value = 50.0; +/// +/// ProgressBar::new(0.0..=100.0, value); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct ProgressBar<Renderer: self::Renderer> { + range: RangeInclusive<f32>, + value: f32, + width: Length, + height: Option<Length>, + style: Renderer::Style, +} + +impl<Renderer: self::Renderer> ProgressBar<Renderer> { + /// Creates a new [`ProgressBar`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`ProgressBar`] + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { + ProgressBar { + value: value.max(*range.start()).min(*range.end()), + range, + width: Length::Fill, + height: None, + style: Renderer::Style::default(), + } + } + + /// Sets the width of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn height(mut self, height: Length) -> Self { + self.height = Some(height); + self + } + + /// Sets the style of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer> +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + .unwrap_or(Length::Units(Renderer::DEFAULT_HEIGHT)) + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height( + self.height + .unwrap_or(Length::Units(Renderer::DEFAULT_HEIGHT)), + ); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> Renderer::Output { + renderer.draw( + layout.bounds(), + self.range.clone(), + self.value, + &self.style, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + self.width.hash(state); + self.height.hash(state); + } +} + +/// The renderer of a [`ProgressBar`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`ProgressBar`] in your user interface. +/// +/// [`ProgressBar`]: struct.ProgressBar.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// The default height of a [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + const DEFAULT_HEIGHT: u16; + + /// Draws a [`ProgressBar`]. + /// + /// It receives: + /// * the bounds of the [`ProgressBar`] + /// * the range of values of the [`ProgressBar`] + /// * the current value of the [`ProgressBar`] + /// * maybe a specific background of the [`ProgressBar`] + /// * maybe a specific active color of the [`ProgressBar`] + /// + /// [`ProgressBar`]: struct.ProgressBar.html + fn draw( + &self, + bounds: Rectangle, + range: RangeInclusive<f32>, + value: f32, + style: &Self::Style, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<ProgressBar<Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'static + self::Renderer, + Message: 'static, +{ + fn from( + progress_bar: ProgressBar<Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(progress_bar) + } +} diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index a9d145db..cdc4862c 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,7 +1,7 @@ //! Create choices using radio buttons. use crate::{ input::{mouse, ButtonState}, - layout, row, text, Align, Color, Element, Event, Font, Hasher, + layout, row, text, Align, Clipboard, Element, Event, Font, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -12,7 +12,8 @@ use std::hash::Hash; /// /// # Example /// ``` -/// # use iced_native::Radio; +/// # type Radio<Message> = +/// # iced_native::Radio<Message, iced_native::renderer::Null>; /// # /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// pub enum Choice { @@ -34,14 +35,14 @@ use std::hash::Hash; /// ///  #[allow(missing_debug_implementations)] -pub struct Radio<Message> { +pub struct Radio<Message, Renderer: self::Renderer> { is_selected: bool, on_click: Message, label: String, - label_color: Option<Color>, + style: Renderer::Style, } -impl<Message> Radio<Message> { +impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> { /// Creates a new [`Radio`] button. /// /// It expects: @@ -61,20 +62,20 @@ impl<Message> Radio<Message> { is_selected: Some(value) == selected, on_click: f(value), label: String::from(label), - label_color: None, + style: Renderer::Style::default(), } } - /// Sets the `Color` of the label of the [`Radio`]. + /// Sets the style of the [`Radio`] button. /// /// [`Radio`]: struct.Radio.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); self } } -impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message> +impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where Renderer: self::Renderer + text::Renderer + row::Renderer, Message: Clone, @@ -95,6 +96,7 @@ where let size = self::Renderer::default_size(renderer); Row::<(), Renderer>::new() + .width(Length::Fill) .spacing(15) .align_items(Align::Center) .push( @@ -113,6 +115,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, ) { match event { Event::Mouse(mouse::Event::Input { @@ -130,6 +133,7 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { @@ -142,11 +146,12 @@ where let label = text::Renderer::draw( renderer, + defaults, label_layout.bounds(), &self.label, - text::Renderer::default_size(renderer), + <Renderer as text::Renderer>::DEFAULT_SIZE, Font::Default, - self.label_color, + None, HorizontalAlignment::Left, VerticalAlignment::Center, ); @@ -159,6 +164,7 @@ where self.is_selected, is_mouse_over, label, + &self.style, ) } @@ -175,6 +181,9 @@ where /// [`Radio`]: struct.Radio.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + /// Returns the default size of a [`Radio`] button. /// /// [`Radio`]: struct.Radio.html @@ -195,16 +204,17 @@ pub trait Renderer: crate::Renderer { is_selected: bool, is_mouse_over: bool, label: Self::Output, + style: &Self::Style, ) -> Self::Output; } -impl<'a, Message, Renderer> From<Radio<Message>> +impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: self::Renderer + row::Renderer + text::Renderer, + Renderer: 'static + self::Renderer + row::Renderer + text::Renderer, Message: 'static + Clone, { - fn from(radio: Radio<Message>) -> Element<'a, Message, Renderer> { + fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(radio) } } diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index c854aff7..775b953e 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -2,7 +2,8 @@ use std::hash::Hash; use crate::{ - layout, Align, Element, Event, Hasher, Layout, Length, Point, Widget, + layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Widget, }; use std::u32; @@ -32,7 +33,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { Row { spacing: 0, padding: 0, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, max_height: u32::MAX, @@ -154,15 +155,17 @@ where cursor_position: Point, messages: &mut Vec<Message>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { self.children.iter_mut().zip(layout.children()).for_each( |(child, layout)| { child.widget.on_event( - event, + event.clone(), layout, cursor_position, messages, renderer, + clipboard, ) }, ); @@ -171,10 +174,11 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { - renderer.draw(&self.children, layout, cursor_position) + renderer.draw(defaults, &self.children, layout, cursor_position) } fn hash_layout(&self, state: &mut Hasher) { @@ -212,6 +216,7 @@ pub trait Renderer: crate::Renderer + Sized { /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, + defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 678d837a..e83f25af 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -2,8 +2,8 @@ use crate::{ column, input::{mouse, ButtonState}, - layout, Align, Column, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length, + Point, Rectangle, Size, Widget, }; use std::{f32, hash::Hash, u32}; @@ -11,14 +11,15 @@ use std::{f32, hash::Hash, u32}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. #[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer> { +pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, content: Column<'a, Message, Renderer>, + style: Renderer::Style, } -impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> { +impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. /// /// [`Scrollable`]: struct.Scrollable.html @@ -29,6 +30,7 @@ impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> { height: Length::Shrink, max_height: u32::MAX, content: Column::new(), + style: Renderer::Style::default(), } } @@ -90,6 +92,14 @@ impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> { self } + /// Sets the style of the [`Scrollable`] . + /// + /// [`Scrollable`]: struct.Scrollable.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } + /// Adds an element to the [`Scrollable`]. /// /// [`Scrollable`]: struct.Scrollable.html @@ -105,7 +115,7 @@ impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer + column::Renderer, + Renderer: 'static + self::Renderer + column::Renderer, { fn width(&self) -> Length { Length::Fill @@ -143,6 +153,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); @@ -150,12 +161,6 @@ where let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let is_mouse_over_scrollbar = renderer.is_mouse_over_scrollbar( - bounds, - content_bounds, - cursor_position, - ); - // TODO: Event capture. Nested scrollables should capture scroll events. if is_mouse_over { match event { @@ -174,49 +179,66 @@ where } } - if self.state.is_scrollbar_grabbed() || is_mouse_over_scrollbar { + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + if self.state.is_scroller_grabbed() { match event { Event::Mouse(mouse::Event::Input { button: mouse::Button::Left, - state, - }) => match state { - ButtonState::Pressed => { - self.state.scroll_to( - cursor_position.y / (bounds.y + bounds.height), - bounds, - content_bounds, - ); - - self.state.scrollbar_grabbed_at = Some(cursor_position); - } - ButtonState::Released => { - self.state.scrollbar_grabbed_at = None; - } - }, + state: ButtonState::Released, + }) => { + self.state.scroller_grabbed_at = None; + } Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if let Some(scrollbar_grabbed_at) = - self.state.scrollbar_grabbed_at + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, self.state.scroller_grabbed_at) { - let ratio = content_bounds.height / bounds.height; - let delta = scrollbar_grabbed_at.y - cursor_position.y; - - self.state.scroll( - delta * ratio, + self.state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), bounds, content_bounds, ); - - self.state.scrollbar_grabbed_at = Some(cursor_position); + } + } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + state: ButtonState::Pressed, + }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) + { + self.state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + self.state.scroller_grabbed_at = + Some(scroller_grabbed_at); + } } } _ => {} } } - let cursor_position = if is_mouse_over - && !(is_mouse_over_scrollbar - || self.state.scrollbar_grabbed_at.is_some()) - { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { Point::new( cursor_position.x, cursor_position.y @@ -236,12 +258,14 @@ where cursor_position, messages, renderer, + clipboard, ) } fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { @@ -249,13 +273,13 @@ where let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = renderer.is_mouse_over_scrollbar( - bounds, - content_bounds, - cursor_position, - ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); let content = { let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { @@ -264,7 +288,12 @@ where Point::new(cursor_position.x, -1.0) }; - self.content.draw(renderer, content_layout, cursor_position) + self.content.draw( + renderer, + defaults, + content_layout, + cursor_position, + ) }; self::Renderer::draw( @@ -274,13 +303,15 @@ where content_layout.bounds(), is_mouse_over, is_mouse_over_scrollbar, + scrollbar, offset, + &self.style, content, ) } fn hash_layout(&self, state: &mut Hasher) { - std::any::TypeId::of::<Scrollable<'static, (), ()>>().hash(state); + std::any::TypeId::of::<Scrollable<'static, (), Renderer>>().hash(state); self.height.hash(state); self.max_height.hash(state); @@ -294,7 +325,7 @@ where /// [`Scrollable`]: struct.Scrollable.html #[derive(Debug, Clone, Copy, Default)] pub struct State { - scrollbar_grabbed_at: Option<Point>, + scroller_grabbed_at: Option<f32>, offset: f32, } @@ -356,12 +387,69 @@ impl State { self.offset.min(hidden_content as f32) as u32 } - /// Returns whether the scrollbar is currently grabbed or not. - pub fn is_scrollbar_grabbed(&self) -> bool { - self.scrollbar_grabbed_at.is_some() + /// Returns whether the scroller is currently grabbed or not. + pub fn is_scroller_grabbed(&self) -> bool { + self.scroller_grabbed_at.is_some() + } +} + +/// The scrollbar of a [`Scrollable`]. +/// +/// [`Scrollable`]: struct.Scrollable.html +#[derive(Debug)] +pub struct Scrollbar { + /// The bounds of the [`Scrollbar`]. + /// + /// [`Scrollbar`]: struct.Scrollbar.html + pub bounds: Rectangle, + + /// The bounds of the [`Scroller`]. + /// + /// [`Scroller`]: struct.Scroller.html + pub scroller: Scroller, +} + +impl Scrollbar { + fn is_mouse_over(&self, cursor_position: Point) -> bool { + self.bounds.contains(cursor_position) + } + + fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { + if self.bounds.contains(cursor_position) { + Some(if self.scroller.bounds.contains(cursor_position) { + (cursor_position.y - self.scroller.bounds.y) + / self.scroller.bounds.height + } else { + 0.5 + }) + } else { + None + } + } + + fn scroll_percentage( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) } } +/// The handle of a [`Scrollbar`]. +/// +/// [`Scrollbar`]: struct.Scrollbar.html +#[derive(Debug, Clone, Copy)] +pub struct Scroller { + /// The bounds of the [`Scroller`]. + /// + /// [`Scroller`]: struct.Scrollbar.html + pub bounds: Rectangle, +} + /// The renderer of a [`Scrollable`]. /// /// Your [renderer] will need to implement this trait before being @@ -370,27 +458,34 @@ impl State { /// [`Scrollable`]: struct.Scrollable.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { - /// Returns whether the mouse is over the scrollbar given the bounds of - /// the [`Scrollable`] and its contents. + /// The style supported by this renderer. + type Style: Default; + + /// Returns the [`Scrollbar`] given the bounds and content bounds of a + /// [`Scrollable`]. /// + /// [`Scrollbar`]: struct.Scrollbar.html /// [`Scrollable`]: struct.Scrollable.html - fn is_mouse_over_scrollbar( + fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, - cursor_position: Point, - ) -> bool; + offset: u32, + ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. /// /// It receives: /// - the [`State`] of the [`Scrollable`] - /// - the bounds of the [`Scrollable`] + /// - the bounds of the [`Scrollable`] widget + /// - the bounds of the [`Scrollable`] content /// - whether the mouse is over the [`Scrollable`] or not - /// - whether the mouse is over the scrollbar or not + /// - whether the mouse is over the [`Scrollbar`] or not + /// - a optional [`Scrollbar`] to be rendered /// - the scrolling offset /// - the drawn content /// + /// [`Scrollbar`]: struct.Scrollbar.html /// [`Scrollable`]: struct.Scrollable.html /// [`State`]: struct.State.html fn draw( @@ -400,7 +495,9 @@ pub trait Renderer: crate::Renderer + Sized { content_bounds: Rectangle, is_mouse_over: bool, is_mouse_over_scrollbar: bool, + scrollbar: Option<Scrollbar>, offset: u32, + style: &Self::Style, content: Self::Output, ) -> Self::Output; } @@ -408,7 +505,7 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + column::Renderer, + Renderer: 'static + self::Renderer + column::Renderer, Message: 'static, { fn from( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index f07ea7cd..008203fe 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -6,8 +6,8 @@ //! [`State`]: struct.State.html use crate::{ input::{mouse, ButtonState}, - layout, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, - Widget, + layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; @@ -21,8 +21,9 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// # Example /// ``` -/// # use iced_native::{slider, Slider}; +/// # use iced_native::{slider, renderer::Null}; /// # +/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; /// pub enum Message { /// SliderChanged(f32), /// } @@ -35,15 +36,16 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message> { +pub struct Slider<'a, Message, Renderer: self::Renderer> { state: &'a mut State, range: RangeInclusive<f32>, value: f32, on_change: Box<dyn Fn(f32) -> Message>, width: Length, + style: Renderer::Style, } -impl<'a, Message> Slider<'a, Message> { +impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// Creates a new [`Slider`]. /// /// It expects: @@ -71,6 +73,7 @@ impl<'a, Message> Slider<'a, Message> { range, on_change: Box::new(on_change), width: Length::Fill, + style: Renderer::Style::default(), } } @@ -81,6 +84,14 @@ impl<'a, Message> Slider<'a, Message> { self.width = width; self } + + /// Sets the style of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } } /// The local state of a [`Slider`]. @@ -100,7 +111,8 @@ impl State { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Slider<'a, Message> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, Message, Renderer> where Renderer: self::Renderer, { @@ -133,6 +145,7 @@ where cursor_position: Point, messages: &mut Vec<Message>, _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, ) { let mut change = || { let bounds = layout.bounds(); @@ -177,6 +190,7 @@ where fn draw( &self, renderer: &mut Renderer, + _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { @@ -186,6 +200,7 @@ where self.range.clone(), self.value, self.state.is_dragging, + &self.style, ) } @@ -202,6 +217,9 @@ where /// [`Slider`]: struct.Slider.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + /// Returns the height of the [`Slider`]. /// /// [`Slider`]: struct.Slider.html @@ -226,16 +244,19 @@ pub trait Renderer: crate::Renderer { range: RangeInclusive<f32>, value: f32, is_dragging: bool, + style: &Self::Style, ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message>> +impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: 'static + self::Renderer, Message: 'static, { - fn from(slider: Slider<'a, Message>) -> Element<'a, Message, Renderer> { + fn from( + slider: Slider<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { Element::new(slider) } } diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs new file mode 100644 index 00000000..24c94bf6 --- /dev/null +++ b/native/src/widget/space.rs @@ -0,0 +1,105 @@ +//! Distribute content vertically. +use std::hash::Hash; + +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; + +/// An amount of empty space. +/// +/// It can be useful if you want to fill some space with nothing. +#[derive(Debug)] +pub struct Space { + width: Length, + height: Length, +} + +impl Space { + /// Creates an amount of empty [`Space`] with the given width and height. + /// + /// [`Space`]: struct.Space.html + pub fn new(width: Length, height: Length) -> Self { + Space { width, height } + } + + /// Creates an amount of horizontal [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_width(width: Length) -> Self { + Space { + width, + height: Length::Shrink, + } + } + + /// Creates an amount of vertical [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_height(height: Length) -> Self { + Space { + width: Length::Shrink, + height, + } + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + layout::Node::new(limits.resolve(Size::ZERO)) + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> Renderer::Output { + renderer.draw(layout.bounds()) + } + + fn hash_layout(&self, state: &mut Hasher) { + std::any::TypeId::of::<Space>().hash(state); + self.width.hash(state); + self.height.hash(state); + } +} + +/// The renderer of an amount of [`Space`]. +/// +/// [`Space`]: struct.Space.html +pub trait Renderer: crate::Renderer { + /// Draws an amount of empty [`Space`]. + /// + /// You should most likely return an empty primitive here. + /// + /// [`Space`]: struct.Space.html + fn draw(&mut self, bounds: Rectangle) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer> +where + Renderer: self::Renderer, + Message: 'static, +{ + fn from(space: Space) -> Element<'a, Message, Renderer> { + Element::new(space) + } +} diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs new file mode 100644 index 00000000..063730bb --- /dev/null +++ b/native/src/widget/svg.rs @@ -0,0 +1,188 @@ +//! Display vector graphics in your application. +use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; + +use std::{ + hash::Hash, + path::{Path, PathBuf}, +}; + +/// A vector graphics image. +/// +/// An [`Svg`] image resizes smoothly without losing any quality. +/// +/// [`Svg`] images can have a considerable rendering cost when resized, +/// specially when they are complex. +/// +/// [`Svg`]: struct.Svg.html +#[derive(Debug, Clone)] +pub struct Svg { + handle: Handle, + width: Length, + height: Length, +} + +impl Svg { + /// Creates a new [`Svg`] from the given [`Handle`]. + /// + /// [`Svg`]: struct.Svg.html + /// [`Handle`]: struct.Handle.html + pub fn new(handle: impl Into<Handle>) -> Self { + Svg { + handle: handle.into(), + width: Length::Fill, + height: Length::Shrink, + } + } + + /// Sets the width of the [`Svg`]. + /// + /// [`Svg`]: struct.Svg.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Svg`]. + /// + /// [`Svg`]: struct.Svg.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Svg +where + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let (width, height) = renderer.dimensions(&self.handle); + + let aspect_ratio = width as f32 / height as f32; + + let mut size = limits + .width(self.width) + .height(self.height) + .resolve(Size::new(width as f32, height as f32)); + + let viewport_aspect_ratio = size.width / size.height; + + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } + + layout::Node::new(size) + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> Renderer::Output { + renderer.draw(self.handle.clone(), layout) + } + + fn hash_layout(&self, state: &mut Hasher) { + self.width.hash(state); + self.height.hash(state); + } +} + +/// An [`Svg`] handle. +/// +/// [`Svg`]: struct.Svg.html +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + path: PathBuf, +} + +impl Handle { + /// Creates an SVG [`Handle`] pointing to the vector image of the given + /// path. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { + use std::hash::Hasher as _; + + let path = path.into(); + + let mut hasher = Hasher::default(); + path.hash(&mut hasher); + + Handle { + id: hasher.finish(), + path, + } + } + + /// Returns the unique identifier of the [`Handle`]. + /// + /// [`Handle`]: struct.Handle.html + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the path of the [`Handle`]. + /// + /// [`Handle`]: enum.Handle.html + pub fn path(&self) -> &Path { + &self.path + } +} + +impl From<String> for Handle { + fn from(path: String) -> Handle { + Handle::from_path(path) + } +} + +impl From<&str> for Handle { + fn from(path: &str) -> Handle { + Handle::from_path(path) + } +} + +/// The renderer of an [`Svg`]. +/// +/// Your [renderer] will need to implement this trait before being able to use +/// an [`Svg`] in your user interface. +/// +/// [`Svg`]: struct.Svg.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// Returns the default dimensions of an [`Svg`] located on the given path. + /// + /// [`Svg`]: struct.Svg.html + fn dimensions(&self, handle: &Handle) -> (u32, u32); + + /// Draws an [`Svg`]. + /// + /// [`Svg`]: struct.Svg.html + fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Svg> for Element<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn from(icon: Svg) -> Element<'a, Message, Renderer> { + Element::new(icon) + } +} diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index cf9c9565..7d8cad6e 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -41,7 +41,7 @@ impl Text { size: None, color: None, font: Font::Default, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, horizontal_alignment: HorizontalAlignment::Left, vertical_alignment: VerticalAlignment::Top, @@ -131,7 +131,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(renderer.default_size()); + let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE); let bounds = limits.max(); @@ -146,13 +146,15 @@ where fn draw( &self, renderer: &mut Renderer, + defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, ) -> Renderer::Output { renderer.draw( + defaults, layout.bounds(), &self.content, - self.size.unwrap_or(renderer.default_size()), + self.size.unwrap_or(Renderer::DEFAULT_SIZE), self.font, self.color, self.horizontal_alignment, @@ -177,10 +179,10 @@ where /// [renderer]: ../../renderer/index.html /// [`UserInterface`]: ../../struct.UserInterface.html pub trait Renderer: crate::Renderer { - /// Returns the default size of the [`Text`]. + /// The default size of [`Text`]. /// /// [`Text`]: struct.Text.html - fn default_size(&self) -> u16; + const DEFAULT_SIZE: u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. @@ -209,6 +211,7 @@ pub trait Renderer: crate::Renderer { /// [`VerticalAlignment`]: enum.VerticalAlignment.html fn draw( &mut self, + defaults: &Self::Defaults, bounds: Rectangle, content: &str, size: u16, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index f97ed424..c068b895 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -6,16 +6,20 @@ //! [`State`]: struct.State.html use crate::{ input::{keyboard, mouse, ButtonState}, - layout, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, - Widget, + layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, + Rectangle, Size, Widget, }; +use std::u32; +use unicode_segmentation::UnicodeSegmentation; + /// A field that can be filled with text. /// /// # Example /// ``` -/// # use iced_native::{text_input, TextInput}; +/// # use iced_native::{text_input, renderer::Null}; /// # +/// # pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Null>; /// #[derive(Debug, Clone)] /// enum Message { /// TextInputChanged(String), @@ -34,19 +38,22 @@ use crate::{ /// ``` ///  #[allow(missing_debug_implementations)] -pub struct TextInput<'a, Message> { +pub struct TextInput<'a, Message, Renderer: self::Renderer> { state: &'a mut State, placeholder: String, value: Value, + is_secure: bool, + font: Font, width: Length, - max_width: Length, + max_width: u32, padding: u16, size: Option<u16>, on_change: Box<dyn Fn(String) -> Message>, on_submit: Option<Message>, + style: Renderer::Style, } -impl<'a, Message> TextInput<'a, Message> { +impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Creates a new [`TextInput`]. /// /// It expects: @@ -66,19 +73,38 @@ impl<'a, Message> TextInput<'a, Message> { where F: 'static + Fn(String) -> Message, { - Self { + TextInput { state, placeholder: String::from(placeholder), value: Value::new(value), + is_secure: false, + font: Font::Default, width: Length::Fill, - max_width: Length::Shrink, + max_width: u32::MAX, padding: 0, size: None, on_change: Box::new(on_change), on_submit: None, + style: Renderer::Style::default(), } } + /// Converts the [`TextInput`] into a secure password input. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + + /// Sets the [`Font`] of the [`Text`]. + /// + /// [`Text`]: struct.Text.html + /// [`Font`]: ../../struct.Font.html + pub fn font(mut self, font: Font) -> Self { + self.font = font; + self + } /// Sets the width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html @@ -90,7 +116,7 @@ impl<'a, Message> TextInput<'a, Message> { /// Sets the maximum width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html - pub fn max_width(mut self, max_width: Length) -> Self { + pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } @@ -119,11 +145,20 @@ impl<'a, Message> TextInput<'a, Message> { self.on_submit = Some(message); self } + + /// Sets the style of the [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: 'static + self::Renderer, Message: Clone + std::fmt::Debug, { fn width(&self) -> Length { @@ -145,11 +180,11 @@ where let limits = limits .pad(padding) .width(self.width) + .max_width(self.max_width) .height(Length::Units(text_size)); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.bounds.x = padding; - text.bounds.y = padding; + text.move_to(Point::new(padding, padding)); layout::Node::with_children(text.size().pad(padding), vec![text]) } @@ -160,22 +195,57 @@ where layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - _renderer: &Renderer, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, ) { match event { Event::Mouse(mouse::Event::Input { button: mouse::Button::Left, state: ButtonState::Pressed, }) => { - self.state.is_focused = - layout.bounds().contains(cursor_position); - - if self.state.cursor_position(&self.value) == 0 { - self.state.move_cursor_to_end(&self.value); + let is_clicked = layout.bounds().contains(cursor_position); + + if is_clicked { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let size = self.size.unwrap_or(renderer.default_size()); + + let offset = renderer.offset( + text_layout.bounds(), + size, + &value, + &self.state, + self.font, + ); + + self.state.cursor_position = find_cursor_position( + renderer, + target + offset, + &value, + size, + 0, + self.value.len(), + self.font, + ); + } else { + self.state.cursor_position = 0; + } } + + self.state.is_focused = is_clicked; } Event::Keyboard(keyboard::Event::CharacterReceived(c)) - if self.state.is_focused && !c.is_control() => + if self.state.is_focused + && self.state.is_pasting.is_none() + && !c.is_control() => { let cursor_position = self.state.cursor_position(&self.value); @@ -188,6 +258,7 @@ where Event::Keyboard(keyboard::Event::Input { key_code, state: ButtonState::Pressed, + modifiers, }) if self.state.is_focused => match key_code { keyboard::KeyCode::Enter => { if let Some(on_submit) = self.on_submit.clone() { @@ -219,10 +290,75 @@ where } } keyboard::KeyCode::Left => { - self.state.move_cursor_left(&self.value); + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + self.state.move_cursor_left_by_words(&self.value); + } else { + self.state.move_cursor_left(&self.value); + } } keyboard::KeyCode::Right => { - self.state.move_cursor_right(&self.value); + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + self.state.move_cursor_right_by_words(&self.value); + } else { + self.state.move_cursor_right(&self.value); + } + } + keyboard::KeyCode::Home => { + self.state.cursor_position = 0; + } + keyboard::KeyCode::End => { + self.state.move_cursor_to_end(&self.value); + } + keyboard::KeyCode::V => { + if platform::is_copy_paste_modifier_pressed(modifiers) { + if let Some(clipboard) = clipboard { + let content = match self.state.is_pasting.take() { + Some(content) => content, + None => { + let content: String = clipboard + .content() + .unwrap_or(String::new()) + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let cursor_position = + self.state.cursor_position(&self.value); + + self.value + .insert_many(cursor_position, content.clone()); + + self.state.move_cursor_right_by_amount( + &self.value, + content.len(), + ); + self.state.is_pasting = Some(content); + + let message = + (self.on_change)(self.value.to_string()); + messages.push(message); + } + } else { + self.state.is_pasting = None; + } + } + _ => {} + }, + Event::Keyboard(keyboard::Event::Input { + key_code, + state: ButtonState::Released, + .. + }) => match key_code { + keyboard::KeyCode::V => { + self.state.is_pasting = None; } _ => {} }, @@ -233,27 +369,44 @@ where fn draw( &self, renderer: &mut Renderer, + _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, ) -> Renderer::Output { let bounds = layout.bounds(); let text_bounds = layout.children().next().unwrap().bounds(); - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value, - &self.state, - ) + if self.is_secure { + renderer.draw( + bounds, + text_bounds, + cursor_position, + self.size.unwrap_or(renderer.default_size()), + self.font, + &self.placeholder, + &self.value.secure(), + &self.state, + &self.style, + ) + } else { + renderer.draw( + bounds, + text_bounds, + cursor_position, + self.size.unwrap_or(renderer.default_size()), + self.font, + &self.placeholder, + &self.value, + &self.state, + &self.style, + ) + } } fn hash_layout(&self, state: &mut Hasher) { use std::{any::TypeId, hash::Hash}; - TypeId::of::<TextInput<'static, ()>>().hash(state); + TypeId::of::<TextInput<'static, (), Renderer>>().hash(state); self.width.hash(state); self.max_width.hash(state); @@ -270,11 +423,36 @@ where /// [`TextInput`]: struct.TextInput.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { + /// The style supported by this renderer. + type Style: Default; + /// Returns the default size of the text of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html fn default_size(&self) -> u16; + /// Returns the width of the value of the [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32; + + /// Returns the current horizontal offset of the value of the + /// [`TextInput`]. + /// + /// This is the amount of horizontal scrolling applied when the [`Value`] + /// does not fit the [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + /// [`Value`]: struct.Value.html + fn offset( + &self, + text_bounds: Rectangle, + size: u16, + value: &Value, + state: &State, + font: Font, + ) -> f32; + /// Draws a [`TextInput`]. /// /// It receives: @@ -294,20 +472,22 @@ pub trait Renderer: crate::Renderer + Sized { text_bounds: Rectangle, cursor_position: Point, size: u16, + font: Font, placeholder: &str, value: &Value, state: &State, + style: &Self::Style, ) -> Self::Output; } -impl<'a, Message, Renderer> From<TextInput<'a, Message>> +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where Renderer: 'static + self::Renderer, Message: 'static + Clone + std::fmt::Debug, { fn from( - text_input: TextInput<'a, Message>, + text_input: TextInput<'a, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(text_input) } @@ -319,7 +499,9 @@ where #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, + is_pasting: Option<Value>, cursor_position: usize, + // TODO: Add stateful horizontal scrolling offset } impl State { @@ -338,6 +520,7 @@ impl State { Self { is_focused: true, + is_pasting: None, cursor_position: usize::MAX, } } @@ -356,26 +539,53 @@ impl State { self.cursor_position.min(value.len()) } + /// Moves the cursor of a [`TextInput`] to the left. + /// + /// [`TextInput`]: struct.TextInput.html + pub(crate) fn move_cursor_left(&mut self, value: &Value) { + let current = self.cursor_position(value); + + if current > 0 { + self.cursor_position = current - 1; + } + } + /// Moves the cursor of a [`TextInput`] to the right. /// /// [`TextInput`]: struct.TextInput.html pub(crate) fn move_cursor_right(&mut self, value: &Value) { + self.move_cursor_right_by_amount(value, 1) + } + + pub(crate) fn move_cursor_right_by_amount( + &mut self, + value: &Value, + amount: usize, + ) { let current = self.cursor_position(value); + let new_position = current.saturating_add(amount); - if current < value.len() { - self.cursor_position = current + 1; + if new_position < value.len() + 1 { + self.cursor_position = new_position; } } - /// Moves the cursor of a [`TextInput`] to the left. + /// Moves the cursor of a [`TextInput`] to the previous start of a word. /// /// [`TextInput`]: struct.TextInput.html - pub(crate) fn move_cursor_left(&mut self, value: &Value) { + pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) { let current = self.cursor_position(value); - if current > 0 { - self.cursor_position = current - 1; - } + self.cursor_position = value.previous_start_of_word(current); + } + + /// Moves the cursor of a [`TextInput`] to the next end of a word. + /// + /// [`TextInput`]: struct.TextInput.html + pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) { + let current = self.cursor_position(value); + + self.cursor_position = value.next_end_of_word(current); } /// Moves the cursor of a [`TextInput`] to the end. @@ -389,51 +599,207 @@ impl State { /// The value of a [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html -// TODO: Use `unicode-segmentation` -#[derive(Debug)] -pub struct Value(Vec<char>); +// TODO: Reduce allocations, cache results (?) +#[derive(Debug, Clone)] +pub struct Value { + graphemes: Vec<String>, +} impl Value { /// Creates a new [`Value`] from a string slice. /// /// [`Value`]: struct.Value.html pub fn new(string: &str) -> Self { - Self(string.chars().collect()) + let graphemes = UnicodeSegmentation::graphemes(string, true) + .map(String::from) + .collect(); + + Self { graphemes } } - /// Returns the total amount of `char` in the [`Value`]. + /// Returns the total amount of graphemes in the [`Value`]. /// /// [`Value`]: struct.Value.html pub fn len(&self) -> usize { - self.0.len() + self.graphemes.len() + } + + /// Returns the position of the previous start of a word from the given + /// grapheme `index`. + /// + /// [`Value`]: struct.Value.html + pub fn previous_start_of_word(&self, index: usize) -> usize { + let previous_string = + &self.graphemes[..index.min(self.graphemes.len())].concat(); + + UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next_back() + .map(|(i, previous_word)| { + index + - UnicodeSegmentation::graphemes(previous_word, true) + .count() + - UnicodeSegmentation::graphemes( + &previous_string[i + previous_word.len()..] as &str, + true, + ) + .count() + }) + .unwrap_or(0) } - /// Returns a new [`Value`] containing the `char` until the given `index`. + /// Returns the position of the next end of a word from the given grapheme + /// `index`. + /// + /// [`Value`]: struct.Value.html + pub fn next_end_of_word(&self, index: usize) -> usize { + let next_string = &self.graphemes[index..].concat(); + + UnicodeSegmentation::split_word_bound_indices(&next_string as &str) + .filter(|(_, word)| !word.trim_start().is_empty()) + .next() + .map(|(i, next_word)| { + index + + UnicodeSegmentation::graphemes(next_word, true).count() + + UnicodeSegmentation::graphemes( + &next_string[..i] as &str, + true, + ) + .count() + }) + .unwrap_or(self.len()) + } + + /// Returns a new [`Value`] containing the graphemes until the given + /// `index`. /// /// [`Value`]: struct.Value.html pub fn until(&self, index: usize) -> Self { - Self(self.0[..index.min(self.len())].to_vec()) + let graphemes = self.graphemes[..index.min(self.len())].to_vec(); + + Self { graphemes } } /// Converts the [`Value`] into a `String`. /// /// [`Value`]: struct.Value.html pub fn to_string(&self) -> String { - use std::iter::FromIterator; - String::from_iter(self.0.iter()) + self.graphemes.concat() } - /// Inserts a new `char` at the given `index`. - /// - /// [`Value`]: struct.Value.html + /// Inserts a new `char` at the given grapheme `index`. pub fn insert(&mut self, index: usize, c: char) { - self.0.insert(index, c); + self.graphemes.insert(index, c.to_string()); + + self.graphemes = + UnicodeSegmentation::graphemes(&self.to_string() as &str, true) + .map(String::from) + .collect(); } - /// Removes the `char` at the given `index`. + /// Inserts a bunch of graphemes at the given grapheme `index`. + pub fn insert_many(&mut self, index: usize, mut value: Value) { + let _ = self + .graphemes + .splice(index..index, value.graphemes.drain(..)); + } + + /// Removes the grapheme at the given `index`. /// /// [`Value`]: struct.Value.html pub fn remove(&mut self, index: usize) { - let _ = self.0.remove(index); + let _ = self.graphemes.remove(index); + } + + /// Returns a new [`Value`] with all its graphemes replaced with the + /// dot ('•') character. + /// + /// [`Value`]: struct.Value.html + pub fn secure(&self) -> Self { + Self { + graphemes: std::iter::repeat(String::from("•")) + .take(self.graphemes.len()) + .collect(), + } + } +} + +// TODO: Reduce allocations +fn find_cursor_position<Renderer: self::Renderer>( + renderer: &Renderer, + target: f32, + value: &Value, + size: u16, + start: usize, + end: usize, + font: Font, +) -> usize { + if start >= end { + if start == 0 { + return 0; + } + + let prev = value.until(start - 1); + let next = value.until(start); + + let prev_width = renderer.measure_value(&prev.to_string(), size, font); + let next_width = renderer.measure_value(&next.to_string(), size, font); + + if next_width - target > target - prev_width { + return start - 1; + } else { + return start; + } + } + + let index = (end - start) / 2; + let subvalue = value.until(start + index); + + let width = renderer.measure_value(&subvalue.to_string(), size, font); + + if width > target { + find_cursor_position( + renderer, + target, + value, + size, + start, + start + index, + font, + ) + } else { + find_cursor_position( + renderer, + target, + value, + size, + start + index + 1, + end, + font, + ) + } +} + +mod platform { + use crate::input::keyboard; + + pub fn is_jump_modifier_pressed( + modifiers: keyboard::ModifiersState, + ) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt + } else { + modifiers.control + } + } + + pub fn is_copy_paste_modifier_pressed( + modifiers: keyboard::ModifiersState, + ) -> bool { + if cfg!(target_os = "macos") { + modifiers.logo + } else { + modifiers.control + } } } diff --git a/native/src/window.rs b/native/src/window.rs new file mode 100644 index 00000000..4dcae62f --- /dev/null +++ b/native/src/window.rs @@ -0,0 +1,6 @@ +//! Build window-based GUI applications. +mod backend; +mod event; + +pub use backend::Backend; +pub use event::Event; diff --git a/native/src/window/backend.rs b/native/src/window/backend.rs new file mode 100644 index 00000000..3bc691cd --- /dev/null +++ b/native/src/window/backend.rs @@ -0,0 +1,55 @@ +use crate::MouseCursor; + +use raw_window_handle::HasRawWindowHandle; + +/// A graphics backend that can render to windows. +pub trait Backend: Sized { + /// The settings of the backend. + type Settings: Default; + + /// The iced renderer of the backend. + type Renderer: crate::Renderer; + + /// The surface of the backend. + type Surface; + + /// The swap chain of the backend. + type SwapChain; + + /// Creates a new [`Backend`] and an associated iced renderer. + /// + /// [`Backend`]: trait.Backend.html + fn new(settings: Self::Settings) -> (Self, Self::Renderer); + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: #associatedtype.Surface + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> Self::Surface; + + /// Crates a new [`SwapChain`] for the given [`Surface`]. + /// + /// [`SwapChain`]: #associatedtype.SwapChain + /// [`Surface`]: #associatedtype.Surface + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> Self::SwapChain; + + /// Draws the output primitives to the next frame of the given [`SwapChain`]. + /// + /// [`SwapChain`]: #associatedtype.SwapChain + /// [`Surface`]: #associatedtype.Surface + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut Self::SwapChain, + output: &<Self::Renderer as crate::Renderer>::Output, + scale_factor: f64, + overlay: &[T], + ) -> MouseCursor; +} diff --git a/native/src/window/event.rs b/native/src/window/event.rs new file mode 100644 index 00000000..b177141a --- /dev/null +++ b/native/src/window/event.rs @@ -0,0 +1,32 @@ +use std::path::PathBuf; + +/// A window-related event. +#[derive(PartialEq, Clone, Debug)] +pub enum Event { + /// A window was resized + Resized { + /// The new width of the window (in units) + width: u32, + + /// The new height of the window (in units) + height: u32, + }, + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted + /// for each file separately. + FileHovered(PathBuf), + + /// A file has beend dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted + /// for each file separately. + FileDropped(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `FilesHoveredLeft` event triggered even if + /// multiple files were hovered. + FilesHoveredLeft, +} diff --git a/src/application.rs b/src/application.rs index a4d20e68..2ee3337f 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,4 @@ -use crate::{Command, Element, Settings}; +use crate::{window, Command, Element, Executor, Settings, Subscription}; /// An interactive cross-platform application. /// @@ -19,7 +19,7 @@ use crate::{Command, Element, Settings}; /// before](index.html#overview). We just need to fill in the gaps: /// /// ```no_run -/// use iced::{button, Application, Button, Column, Command, Element, Settings, Text}; +/// use iced::{button, executor, Application, Button, Column, Command, Element, Settings, Text}; /// /// pub fn main() { /// Counter::run(Settings::default()) @@ -39,6 +39,7 @@ use crate::{Command, Element, Settings}; /// } /// /// impl Application for Counter { +/// type Executor = executor::Null; /// type Message = Message; /// /// fn new() -> (Self, Command<Message>) { @@ -80,10 +81,18 @@ use crate::{Command, Element, Settings}; /// } /// ``` pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [`executor::Default`] can be a good starting point! + /// + /// [`Executor`]: trait.Executor.html + /// [`executor::Default`]: executor/struct.Default.html + type Executor: Executor; + /// The type of __messages__ your [`Application`] will produce. /// /// [`Application`]: trait.Application.html - type Message: std::fmt::Debug + Send + Clone; + type Message: std::fmt::Debug + Send; /// Initializes the [`Application`]. /// @@ -117,6 +126,20 @@ pub trait Application: Sized { /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. @@ -124,6 +147,20 @@ pub trait Application: Sized { /// [`Application`]: trait.Application.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the current [`Application`] mode. + /// + /// The runtime will automatically transition your application if a new mode + /// is returned. + /// + /// Currently, the mode only has an effect in native platforms. + /// + /// By default, an application will run in windowed mode. + /// + /// [`Application`]: trait.Application.html + fn mode(&self) -> window::Mode { + window::Mode::Windowed + } + /// Runs the [`Application`]. /// /// This method will take control of the current thread and __will NOT @@ -132,12 +169,23 @@ pub trait Application: Sized { /// It should probably be that last thing you call in your `main` function. /// /// [`Application`]: trait.Application.html - fn run(settings: Settings) + fn run(_settings: Settings) where Self: 'static, { #[cfg(not(target_arch = "wasm32"))] - <Instance<Self> as iced_winit::Application>::run(settings.into()); + <Instance<Self> as iced_winit::Application>::run( + _settings.into(), + iced_wgpu::Settings { + default_font: _settings.default_font, + antialiasing: if _settings.antialiasing { + Some(iced_wgpu::settings::Antialiasing::MSAAx4) + } else { + None + }, + ..iced_wgpu::Settings::default() + }, + ); #[cfg(target_arch = "wasm32")] <Instance<Self> as iced_web::Application>::run(); @@ -151,7 +199,8 @@ impl<A> iced_winit::Application for Instance<A> where A: Application, { - type Renderer = iced_wgpu::Renderer; + type Backend = iced_wgpu::window::Backend; + type Executor = A::Executor; type Message = A::Message; fn new() -> (Self, Command<A::Message>) { @@ -164,10 +213,21 @@ where self.0.title() } + fn mode(&self) -> iced_winit::Mode { + match self.0.mode() { + window::Mode::Windowed => iced_winit::Mode::Windowed, + window::Mode::Fullscreen => iced_winit::Mode::Fullscreen, + } + } + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { self.0.update(message) } + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() + } + fn view(&mut self) -> Element<'_, Self::Message> { self.0.view() } @@ -179,6 +239,7 @@ where A: Application, { type Message = A::Message; + type Executor = A::Executor; fn new() -> (Self, Command<A::Message>) { let (app, command) = A::new(); @@ -194,6 +255,10 @@ where self.0.update(message) } + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() + } + fn view(&mut self) -> Element<'_, Self::Message> { self.0.view() } diff --git a/src/element.rs b/src/element.rs new file mode 100644 index 00000000..e5356fb6 --- /dev/null +++ b/src/element.rs @@ -0,0 +1,9 @@ +/// A generic widget. +/// +/// This is an alias of an `iced_native` element with a default `Renderer`. +#[cfg(not(target_arch = "wasm32"))] +pub type Element<'a, Message> = + iced_winit::Element<'a, Message, iced_wgpu::Renderer>; + +#[cfg(target_arch = "wasm32")] +pub use iced_web::Element; diff --git a/src/executor.rs b/src/executor.rs new file mode 100644 index 00000000..b4be5264 --- /dev/null +++ b/src/executor.rs @@ -0,0 +1,68 @@ +//! Choose your preferred executor to power your application. +pub use crate::common::{executor::Null, Executor}; + +pub use platform::Default; + +#[cfg(not(target_arch = "wasm32"))] +mod platform { + use iced_futures::{executor, futures}; + + #[cfg(feature = "tokio")] + type Executor = executor::Tokio; + + #[cfg(all(not(feature = "tokio"), feature = "async-std"))] + type Executor = executor::AsyncStd; + + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + type Executor = executor::ThreadPool; + + /// A default cross-platform executor. + /// + /// - On native platforms, it will use: + /// - `iced_futures::executor::Tokio` when the `tokio` feature is enabled. + /// - `iced_futures::executor::AsyncStd` when the `async-std` feature is + /// enabled. + /// - `iced_futures::executor::ThreadPool` otherwise. + /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. + #[derive(Debug)] + pub struct Default(Executor); + + impl super::Executor for Default { + fn new() -> Result<Self, futures::io::Error> { + Ok(Default(Executor::new()?)) + } + + fn spawn( + &self, + future: impl futures::Future<Output = ()> + Send + 'static, + ) { + let _ = self.0.spawn(future); + } + + fn enter<R>(&self, f: impl FnOnce() -> R) -> R { + self.0.enter(f) + } + } +} + +#[cfg(target_arch = "wasm32")] +mod platform { + use iced_futures::{executor::WasmBindgen, futures, Executor}; + + /// A default cross-platform executor. + /// + /// - On native platforms, it will use `iced_futures::executor::ThreadPool`. + /// - On the Web, it will use `iced_futures::executor::WasmBindgen`. + #[derive(Debug)] + pub struct Default(WasmBindgen); + + impl Executor for Default { + fn new() -> Result<Self, futures::io::Error> { + Ok(Default(WasmBindgen::new()?)) + } + + fn spawn(&self, future: impl futures::Future<Output = ()> + 'static) { + self.0.spawn(future); + } + } +} @@ -171,23 +171,38 @@ //! //! [Elm]: https://elm-lang.org/ //! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [documentation]: https://docs.rs/iced //! [examples]: https://github.com/hecrj/iced/tree/master/examples //! [`Application`]: trait.Application.html #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] mod application; -#[cfg_attr(target_arch = "wasm32", path = "web.rs")] -#[cfg_attr(not(target_arch = "wasm32"), path = "native.rs")] -mod platform; +mod element; mod sandbox; +pub mod executor; pub mod settings; +pub mod widget; +pub mod window; + +#[doc(no_inline)] +pub use widget::*; pub use application::Application; -pub use platform::*; +pub use element::Element; +pub use executor::Executor; pub use sandbox::Sandbox; pub use settings::Settings; + +#[cfg(not(target_arch = "wasm32"))] +use iced_winit as common; + +#[cfg(target_arch = "wasm32")] +use iced_web as common; + +pub use common::{ + futures, Align, Background, Color, Command, Font, HorizontalAlignment, + Length, Point, Size, Space, Subscription, Vector, VerticalAlignment, +}; diff --git a/src/native.rs b/src/native.rs deleted file mode 100644 index 926b2d11..00000000 --- a/src/native.rs +++ /dev/null @@ -1,113 +0,0 @@ -pub use iced_winit::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - VerticalAlignment, -}; - -pub mod widget { - //! Display information and interactive controls in your application. - //! - //! # Re-exports - //! For convenience, the contents of this module are available at the root - //! module. Therefore, you can directly type: - //! - //! ``` - //! use iced::{button, Button}; - //! ``` - //! - //! # Stateful widgets - //! Some widgets need to keep track of __local state__. - //! - //! These widgets have their own module with a `State` type. For instance, a - //! [`TextInput`] has some [`text_input::State`]. - //! - //! [`TextInput`]: text_input/struct.TextInput.html - //! [`text_input::State`]: text_input/struct.State.html - pub mod button { - //! Allow your users to perform actions by pressing a button. - //! - //! A [`Button`] has some local [`State`]. - //! - //! [`Button`]: type.Button.html - //! [`State`]: struct.State.html - - /// A widget that produces a message when clicked. - /// - /// This is an alias of an `iced_native` button with a default - /// `Renderer`. - pub type Button<'a, Message> = - iced_winit::Button<'a, Message, iced_wgpu::Renderer>; - - pub use iced_winit::button::State; - } - - pub mod scrollable { - //! Navigate an endless amount of content with a scrollbar. - - /// A widget that can vertically display an infinite amount of content - /// with a scrollbar. - /// - /// This is an alias of an `iced_native` scrollable with a default - /// `Renderer`. - pub type Scrollable<'a, Message> = - iced_winit::Scrollable<'a, Message, iced_wgpu::Renderer>; - - pub use iced_winit::scrollable::State; - } - - pub mod text_input { - //! Ask for information using text fields. - //! - //! A [`TextInput`] has some local [`State`]. - //! - //! [`TextInput`]: struct.TextInput.html - //! [`State`]: struct.State.html - pub use iced_winit::text_input::{State, TextInput}; - } - - pub mod slider { - //! Display an interactive selector of a single value from a range of - //! values. - //! - //! A [`Slider`] has some local [`State`]. - //! - //! [`Slider`]: struct.Slider.html - //! [`State`]: struct.State.html - pub use iced_winit::slider::{Slider, State}; - } - - pub use iced_winit::{Checkbox, Image, Radio, Text}; - - #[doc(no_inline)] - pub use { - button::Button, scrollable::Scrollable, slider::Slider, - text_input::TextInput, - }; - - /// A container that distributes its contents vertically. - /// - /// This is an alias of an `iced_native` column with a default `Renderer`. - pub type Column<'a, Message> = - iced_winit::Column<'a, Message, iced_wgpu::Renderer>; - - /// A container that distributes its contents horizontally. - /// - /// This is an alias of an `iced_native` row with a default `Renderer`. - pub type Row<'a, Message> = - iced_winit::Row<'a, Message, iced_wgpu::Renderer>; - - /// An element decorating some content. - /// - /// This is an alias of an `iced_native` container with a default - /// `Renderer`. - pub type Container<'a, Message> = - iced_winit::Container<'a, Message, iced_wgpu::Renderer>; -} - -#[doc(no_inline)] -pub use widget::*; - -/// A generic widget. -/// -/// This is an alias of an `iced_native` element with a default `Renderer`. -pub type Element<'a, Message> = - iced_winit::Element<'a, Message, iced_wgpu::Renderer>; diff --git a/src/sandbox.rs b/src/sandbox.rs index acf7f5e0..2c0332ff 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,4 @@ -use crate::{Application, Command, Element, Settings}; +use crate::{executor, Application, Command, Element, Settings, Subscription}; /// A sandboxed [`Application`]. /// @@ -81,7 +81,7 @@ pub trait Sandbox { /// The type of __messages__ your [`Sandbox`] will produce. /// /// [`Sandbox`]: trait.Sandbox.html - type Message: std::fmt::Debug + Send + Clone; + type Message: std::fmt::Debug + Send; /// Initializes the [`Sandbox`]. /// @@ -133,6 +133,7 @@ impl<T> Application for T where T: Sandbox, { + type Executor = executor::Null; type Message = T::Message; fn new() -> (Self, Command<T::Message>) { @@ -149,6 +150,10 @@ where Command::none() } + fn subscription(&self) -> Subscription<T::Message> { + Subscription::none() + } + fn view(&mut self) -> Element<'_, T::Message> { T::view(self) } diff --git a/src/settings.rs b/src/settings.rs index 2556c51b..32ec583c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,33 +1,30 @@ //! Configure your application. +use crate::window; /// The settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Settings { - /// The [`Window`] settings. + /// The window settings. /// /// They will be ignored on the Web. /// /// [`Window`]: struct.Window.html - pub window: Window, -} - -/// The window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Window { - /// The size of the window. - pub size: (u32, u32), + pub window: window::Settings, - /// Whether the window should be resizable or not. - pub resizable: bool, -} + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + // TODO: Add `name` for web compatibility + pub default_font: Option<&'static [u8]>, -impl Default for Window { - fn default() -> Window { - Window { - size: (1024, 768), - resizable: true, - } - } + /// If set to true, the renderer will try to perform antialiasing for some + /// primitives. + /// + /// Enabling it can produce a smoother result in some widgets, like the + /// `Canvas`, at a performance cost. + /// + /// By default, it is disabled. + pub antialiasing: bool, } #[cfg(not(target_arch = "wasm32"))] @@ -37,6 +34,8 @@ impl From<Settings> for iced_winit::Settings { window: iced_winit::settings::Window { size: settings.window.size, resizable: settings.window.resizable, + decorations: settings.window.decorations, + platform_specific: Default::default(), }, } } diff --git a/src/web.rs b/src/web.rs deleted file mode 100644 index 31f1a6fc..00000000 --- a/src/web.rs +++ /dev/null @@ -1 +0,0 @@ -pub use iced_web::*; diff --git a/src/widget.rs b/src/widget.rs new file mode 100644 index 00000000..7d3a1cef --- /dev/null +++ b/src/widget.rs @@ -0,0 +1,60 @@ +//! Display information and interactive controls in your application. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced::{button, Button}; +//! ``` +//! +//! # Stateful widgets +//! Some widgets need to keep track of __local state__. +//! +//! These widgets have their own module with a `State` type. For instance, a +//! [`TextInput`] has some [`text_input::State`]. +//! +//! [`TextInput`]: text_input/struct.TextInput.html +//! [`text_input::State`]: text_input/struct.State.html +#[cfg(not(target_arch = "wasm32"))] +mod platform { + pub use iced_wgpu::widget::*; + + pub mod image { + //! Display images in your user interface. + pub use iced_winit::image::{Handle, Image}; + } + + pub mod svg { + //! Display vector graphics in your user interface. + pub use iced_winit::svg::{Handle, Svg}; + } + + pub use iced_winit::Text; + + #[doc(no_inline)] + pub use { + button::Button, checkbox::Checkbox, container::Container, image::Image, + progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable, + slider::Slider, svg::Svg, text_input::TextInput, + }; + + /// A container that distributes its contents vertically. + /// + /// This is an alias of an `iced_native` column with a default `Renderer`. + pub type Column<'a, Message> = + iced_winit::Column<'a, Message, iced_wgpu::Renderer>; + + /// A container that distributes its contents horizontally. + /// + /// This is an alias of an `iced_native` row with a default `Renderer`. + pub type Row<'a, Message> = + iced_winit::Row<'a, Message, iced_wgpu::Renderer>; +} + +#[cfg(target_arch = "wasm32")] +mod platform { + pub use iced_web::widget::*; +} + +pub use platform::*; diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 00000000..54ea2a02 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,6 @@ +//! Configure the window of your application in native platforms. +mod mode; +mod settings; + +pub use mode::Mode; +pub use settings::Settings; diff --git a/src/window/mode.rs b/src/window/mode.rs new file mode 100644 index 00000000..37464711 --- /dev/null +++ b/src/window/mode.rs @@ -0,0 +1,9 @@ +/// The mode of a window-based application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// The application appears in its own window. + Windowed, + + /// The application takes the whole screen of its current monitor. + Fullscreen, +} diff --git a/winit/src/settings.rs b/src/window/settings.rs index d257ecd8..a31d2af2 100644 --- a/winit/src/settings.rs +++ b/src/window/settings.rs @@ -1,29 +1,22 @@ -//! Configure your application. - -/// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Settings { - /// The [`Window`] settings - /// - /// [`Window`]: struct.Window.html - pub window: Window, -} - /// The window settings of an application. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Window { +pub struct Settings { /// The size of the window. pub size: (u32, u32), /// Whether the window should be resizable or not. pub resizable: bool, + + /// Whether the window should have a border, a title bar, etc. or not. + pub decorations: bool, } -impl Default for Window { - fn default() -> Window { - Window { +impl Default for Settings { + fn default() -> Settings { + Settings { size: (1024, 768), resizable: true, + decorations: true, } } } diff --git a/style/Cargo.toml b/style/Cargo.toml new file mode 100644 index 00000000..5928c60d --- /dev/null +++ b/style/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "iced_style" +version = "0.1.0-alpha" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "The default set of styles of Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_style" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] + +[dependencies] +iced_core = { version = "0.1.0", path = "../core" } diff --git a/style/src/button.rs b/style/src/button.rs new file mode 100644 index 00000000..1e3844f9 --- /dev/null +++ b/style/src/button.rs @@ -0,0 +1,96 @@ +//! Allow your users to perform actions by pressing a button. +use iced_core::{Background, Color, Vector}; + +/// The appearance of a button. +#[derive(Debug)] +pub struct Style { + pub shadow_offset: Vector, + pub background: Option<Background>, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, + pub text_color: Color, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + shadow_offset: Vector::default(), + background: None, + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + text_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of a button. +pub trait StyleSheet { + fn active(&self) -> Style; + + fn hovered(&self) -> Style { + let active = self.active(); + + Style { + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + + fn pressed(&self) -> Style { + Style { + shadow_offset: Vector::default(), + ..self.active() + } + } + + fn disabled(&self) -> Style { + let active = self.active(); + + Style { + shadow_offset: Vector::default(), + background: active.background.map(|background| match background { + Background::Color(color) => Background::Color(Color { + a: color.a * 0.5, + ..color + }), + }), + text_color: Color { + a: active.text_color.a * 0.5, + ..active.text_color + }, + ..active + } + } +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self) -> Style { + Style { + shadow_offset: Vector::new(0.0, 0.0), + background: Some(Background::Color([0.87, 0.87, 0.87].into())), + border_radius: 2, + border_width: 1, + border_color: [0.7, 0.7, 0.7].into(), + text_color: Color::BLACK, + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs new file mode 100644 index 00000000..3c645f15 --- /dev/null +++ b/style/src/checkbox.rs @@ -0,0 +1,55 @@ +//! Show toggle controls using checkboxes. +use iced_core::{Background, Color}; + +/// The appearance of a checkbox. +#[derive(Debug)] +pub struct Style { + pub background: Background, + pub checkmark_color: Color, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, +} + +/// A set of rules that dictate the style of a checkbox. +pub trait StyleSheet { + fn active(&self, is_checked: bool) -> Style; + + fn hovered(&self, is_checked: bool) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self, _is_checked: bool) -> Style { + Style { + background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)), + checkmark_color: Color::from_rgb(0.3, 0.3, 0.3), + border_radius: 5, + border_width: 1, + border_color: Color::from_rgb(0.6, 0.6, 0.6), + } + } + + fn hovered(&self, is_checked: bool) -> Style { + Style { + background: Background::Color(Color::from_rgb(0.90, 0.90, 0.90)), + ..self.active(is_checked) + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/container.rs b/style/src/container.rs new file mode 100644 index 00000000..d2247342 --- /dev/null +++ b/style/src/container.rs @@ -0,0 +1,59 @@ +//! Decorate content and apply alignment. +use iced_core::{Background, Color}; + +/// The appearance of a container. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub text_color: Option<Color>, + pub background: Option<Background>, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + text_color: None, + background: None, + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } +} + +/// A set of rules that dictate the style of a container. +pub trait StyleSheet { + /// Produces the style of a container. + fn style(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn style(&self) -> Style { + Style { + text_color: None, + background: None, + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/lib.rs b/style/src/lib.rs new file mode 100644 index 00000000..2c5977b5 --- /dev/null +++ b/style/src/lib.rs @@ -0,0 +1,12 @@ +//! The styling library of Iced. +//! +//! It contains a set of styles and stylesheets for most of the built-in +//! widgets. +pub mod button; +pub mod checkbox; +pub mod container; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs new file mode 100644 index 00000000..73503fa8 --- /dev/null +++ b/style/src/progress_bar.rs @@ -0,0 +1,42 @@ +//! Provide progress feedback to your users. +use iced_core::{Background, Color}; + +/// The appearance of a progress bar. +#[derive(Debug)] +pub struct Style { + pub background: Background, + pub bar: Background, + pub border_radius: u16, +} + +/// A set of rules that dictate the style of a progress bar. +pub trait StyleSheet { + fn style(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn style(&self) -> Style { + Style { + background: Background::Color(Color::from_rgb(0.6, 0.6, 0.6)), + bar: Background::Color(Color::from_rgb(0.3, 0.9, 0.3)), + border_radius: 5, + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/radio.rs b/style/src/radio.rs new file mode 100644 index 00000000..1f0689b9 --- /dev/null +++ b/style/src/radio.rs @@ -0,0 +1,53 @@ +//! Create choices using radio buttons. +use iced_core::{Background, Color}; + +/// The appearance of a radio button. +#[derive(Debug)] +pub struct Style { + pub background: Background, + pub dot_color: Color, + pub border_width: u16, + pub border_color: Color, +} + +/// A set of rules that dictate the style of a radio button. +pub trait StyleSheet { + fn active(&self) -> Style; + + fn hovered(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self) -> Style { + Style { + background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)), + dot_color: Color::from_rgb(0.3, 0.3, 0.3), + border_width: 1, + border_color: Color::from_rgb(0.6, 0.6, 0.6), + } + } + + fn hovered(&self) -> Style { + Style { + background: Background::Color(Color::from_rgb(0.90, 0.90, 0.90)), + ..self.active() + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs new file mode 100644 index 00000000..690c14a2 --- /dev/null +++ b/style/src/scrollable.rs @@ -0,0 +1,76 @@ +//! Navigate an endless amount of content with a scrollbar. +use iced_core::{Background, Color}; + +/// The appearance of a scrollable. +#[derive(Debug, Clone, Copy)] +pub struct Scrollbar { + pub background: Option<Background>, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, + pub scroller: Scroller, +} + +/// The appearance of the scroller of a scrollable. +#[derive(Debug, Clone, Copy)] +pub struct Scroller { + pub color: Color, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, +} + +/// A set of rules that dictate the style of a scrollable. +pub trait StyleSheet { + /// Produces the style of an active scrollbar. + fn active(&self) -> Scrollbar; + + /// Produces the style of an hovered scrollbar. + fn hovered(&self) -> Scrollbar; + + /// Produces the style of a scrollbar that is being dragged. + fn dragging(&self) -> Scrollbar { + self.hovered() + } +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self) -> Scrollbar { + Scrollbar { + background: None, + border_radius: 5, + border_width: 0, + border_color: Color::TRANSPARENT, + scroller: Scroller { + color: [0.0, 0.0, 0.0, 0.7].into(), + border_radius: 5, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> Scrollbar { + Scrollbar { + background: Some(Background::Color([0.0, 0.0, 0.0, 0.3].into())), + ..self.active() + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/slider.rs b/style/src/slider.rs new file mode 100644 index 00000000..776e180c --- /dev/null +++ b/style/src/slider.rs @@ -0,0 +1,95 @@ +//! Display an interactive selector of a single value from a range of values. +use iced_core::Color; + +/// The appearance of a slider. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub rail_colors: (Color, Color), + pub handle: Handle, +} + +/// The appearance of the handle of a slider. +#[derive(Debug, Clone, Copy)] +pub struct Handle { + pub shape: HandleShape, + pub color: Color, + pub border_width: u16, + pub border_color: Color, +} + +/// The shape of the handle of a slider. +#[derive(Debug, Clone, Copy)] +pub enum HandleShape { + Circle { radius: u16 }, + Rectangle { width: u16, border_radius: u16 }, +} + +/// A set of rules that dictate the style of a slider. +pub trait StyleSheet { + /// Produces the style of an active slider. + fn active(&self) -> Style; + + /// Produces the style of an hovered slider. + fn hovered(&self) -> Style; + + /// Produces the style of a slider that is being dragged. + fn dragging(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self) -> Style { + Style { + rail_colors: ([0.6, 0.6, 0.6, 0.5].into(), Color::WHITE), + handle: Handle { + shape: HandleShape::Rectangle { + width: 8, + border_radius: 4, + }, + color: Color::from_rgb(0.95, 0.95, 0.95), + border_color: Color::from_rgb(0.6, 0.6, 0.6), + border_width: 1, + }, + } + } + + fn hovered(&self) -> Style { + let active = self.active(); + + Style { + handle: Handle { + color: Color::from_rgb(0.90, 0.90, 0.90), + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> Style { + let active = self.active(); + + Style { + handle: Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/style/src/text_input.rs b/style/src/text_input.rs new file mode 100644 index 00000000..c5123b20 --- /dev/null +++ b/style/src/text_input.rs @@ -0,0 +1,83 @@ +//! Display fields that can be filled with text. +use iced_core::{Background, Color}; + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub background: Background, + pub border_radius: u16, + pub border_width: u16, + pub border_color: Color, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + background: Background::Color(Color::WHITE), + border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } +} + +/// A set of rules that dictate the style of a text input. +pub trait StyleSheet { + /// Produces the style of an active text input. + fn active(&self) -> Style; + + /// Produces the style of a focused text input. + fn focused(&self) -> Style; + + fn placeholder_color(&self) -> Color; + + fn value_color(&self) -> Color; + + /// Produces the style of an hovered text input. + fn hovered(&self) -> Style { + self.focused() + } +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self) -> Style { + Style { + background: Background::Color(Color::WHITE), + border_radius: 5, + border_width: 1, + border_color: Color::from_rgb(0.7, 0.7, 0.7), + } + } + + fn focused(&self) -> Style { + Style { + border_color: Color::from_rgb(0.5, 0.5, 0.5), + ..self.active() + } + } + + fn placeholder_color(&self) -> Color { + Color::from_rgb(0.7, 0.7, 0.7) + } + + fn value_color(&self) -> Color { + Color::from_rgb(0.3, 0.3, 0.3) + } +} + +impl std::default::Default for Box<dyn StyleSheet> { + fn default() -> Self { + Box::new(Default) + } +} + +impl<T> From<T> for Box<dyn StyleSheet> +where + T: 'static + StyleSheet, +{ + fn from(style: T) -> Self { + Box::new(style) + } +} diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index 49ddf5a3..ed02519a 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- `Button::background` takes an `Into<Background>` now instead of a `Background`. + +### Fixed +- Render not being scheduled after `Command` futures finishing. ## [0.1.0] - 2019-11-25 ### Added diff --git a/web/Cargo.toml b/web/Cargo.toml index 8c559d73..c043c697 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,11 +15,19 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] -iced_core = { version = "0.1.0", path = "../core", features = ["command"] } +iced_style = { version = "0.1.0-alpha", path = "../style" } dodrio = "0.1.0" wasm-bindgen = "0.2.51" wasm-bindgen-futures = "0.4" -futures = "0.3" +url = "2.0" + +[dependencies.iced_core] +version = "0.1.0" +path = "../core" + +[dependencies.iced_futures] +version = "0.1.0-alpha" +path = "../futures" [dependencies.web-sys] version = "0.3.27" @@ -31,4 +39,5 @@ features = [ "Event", "EventTarget", "InputEvent", + "KeyboardEvent", ] diff --git a/web/README.md b/web/README.md index 762f6c83..aceb4a97 100644 --- a/web/README.md +++ b/web/README.md @@ -18,7 +18,7 @@ The crate is currently a __very experimental__, simple abstraction layer over [` Add `iced_web` as a dependency in your `Cargo.toml`: ```toml -iced_web = "0.1.0-alpha" +iced_web = "0.1.0" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If @@ -35,11 +35,15 @@ For instance, let's say we want to build the [`tour` example]: ``` cd examples -cargo build --example tour --target wasm32-unknown-unknown +cargo build --package tour --target wasm32-unknown-unknown wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web ``` -Then, we need to create an `.html` file to load our application: +*__Note:__ Keep in mind that Iced is still in early exploration stages and most of the work needs to happen on the native side of the ecosystem. At this stage, it is important to be able to batch work without having to constantly jump back and forth. Because of this, there is currently no requirement for the `master` branch to contain a cross-platform API at all times. If you hit an issue when building an example and want to help, it may be a good way to [start contributing]!* + +[start contributing]: ../CONTRIBUTING.md + +Once the example is compiled, we need to create an `.html` file to load our application: ```html <!DOCTYPE html> diff --git a/web/src/bus.rs b/web/src/bus.rs index 1b650b28..c66e9659 100644 --- a/web/src/bus.rs +++ b/web/src/bus.rs @@ -1,5 +1,4 @@ -use crate::Instance; - +use iced_futures::futures::channel::mpsc; use std::rc::Rc; /// A publisher of messages. @@ -8,21 +7,26 @@ use std::rc::Rc; /// /// [`Application`]: trait.Application.html #[allow(missing_debug_implementations)] -#[derive(Clone)] pub struct Bus<Message> { - publish: Rc<Box<dyn Fn(Message, &mut dyn dodrio::RootRender)>>, + publish: Rc<Box<dyn Fn(Message) -> ()>>, +} + +impl<Message> Clone for Bus<Message> { + fn clone(&self) -> Self { + Bus { + publish: self.publish.clone(), + } + } } impl<Message> Bus<Message> where - Message: 'static + Clone, + Message: 'static, { - pub(crate) fn new() -> Self { + pub(crate) fn new(publish: mpsc::UnboundedSender<Message>) -> Self { Self { - publish: Rc::new(Box::new(|message, root| { - let app = root.unwrap_mut::<Instance<Message>>(); - - app.update(message) + publish: Rc::new(Box::new(move |message| { + publish.unbounded_send(message).expect("Send message"); })), } } @@ -30,8 +34,8 @@ where /// Publishes a new message for the [`Application`]. /// /// [`Application`]: trait.Application.html - pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) { - (self.publish)(message, root); + pub fn publish(&self, message: Message) { + (self.publish)(message) } /// Creates a new [`Bus`] that applies the given function to the messages @@ -45,9 +49,7 @@ where let publish = self.publish.clone(); Bus { - publish: Rc::new(Box::new(move |message, root| { - publish(mapper(message), root) - })), + publish: Rc::new(Box::new(move |message| publish(mapper(message)))), } } } diff --git a/web/src/style.rs b/web/src/css.rs index 2fb8602a..6a307770 100644 --- a/web/src/style.rs +++ b/web/src/css.rs @@ -1,11 +1,11 @@ //! Style your widgets. -use crate::{bumpalo, Align, Color, Length}; +use crate::{bumpalo, Align, Background, Color, Length}; use std::collections::BTreeMap; -/// The style of a VDOM node. +/// A CSS rule of a VDOM node. #[derive(Debug)] -pub enum Style { +pub enum Rule { /// Container with vertical distribution Column, @@ -19,16 +19,16 @@ pub enum Style { Spacing(u16), } -impl Style { +impl Rule { /// Returns the class name of the [`Style`]. /// /// [`Style`]: enum.Style.html pub fn class<'a>(&self) -> String { match self { - Style::Column => String::from("c"), - Style::Row => String::from("r"), - Style::Padding(padding) => format!("p-{}", padding), - Style::Spacing(spacing) => format!("s-{}", spacing), + Rule::Column => String::from("c"), + Rule::Row => String::from("r"), + Rule::Padding(padding) => format!("p-{}", padding), + Rule::Spacing(spacing) => format!("s-{}", spacing), } } @@ -39,24 +39,24 @@ impl Style { let class = self.class(); match self { - Style::Column => { + Rule::Column => { let body = "{ display: flex; flex-direction: column; }"; bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() } - Style::Row => { + Rule::Row => { let body = "{ display: flex; flex-direction: row; }"; bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() } - Style::Padding(padding) => bumpalo::format!( + Rule::Padding(padding) => bumpalo::format!( in bump, ".{} {{ box-sizing: border-box; padding: {}px }}", class, padding ) .into_bump_str(), - Style::Spacing(spacing) => bumpalo::format!( + Rule::Spacing(spacing) => bumpalo::format!( in bump, ".c.{} > * {{ margin-bottom: {}px }} \ .r.{} > * {{ margin-right: {}px }} \ @@ -74,34 +74,34 @@ impl Style { } } -/// A sheet of styles. +/// A cascading style sheet. #[derive(Debug)] -pub struct Sheet<'a> { - styles: BTreeMap<String, &'a str>, +pub struct Css<'a> { + rules: BTreeMap<String, &'a str>, } -impl<'a> Sheet<'a> { +impl<'a> Css<'a> { /// Creates an empty style [`Sheet`]. /// /// [`Sheet`]: struct.Sheet.html pub fn new() -> Self { - Self { - styles: BTreeMap::new(), + Css { + rules: BTreeMap::new(), } } - /// Inserts the [`Style`] in the [`Sheet`], if it was not previously + /// Inserts the [`rule`] in the [`Sheet`], if it was not previously /// inserted. /// - /// It returns the class name of the provided [`Style`]. + /// It returns the class name of the provided [`Rule`]. /// /// [`Sheet`]: struct.Sheet.html - /// [`Style`]: enum.Style.html - pub fn insert(&mut self, bump: &'a bumpalo::Bump, style: Style) -> String { - let class = style.class(); + /// [`Rule`]: enum.Rule.html + pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String { + let class = rule.class(); - if !self.styles.contains_key(&class) { - let _ = self.styles.insert(class.clone(), style.declaration(bump)); + if !self.rules.contains_key(&class) { + let _ = self.rules.insert(class.clone(), rule.declaration(bump)); } class @@ -119,12 +119,12 @@ impl<'a> Sheet<'a> { declarations.push(text( "body { height: 100%; margin: 0; padding: 0; font-family: sans-serif }", )); - declarations.push(text("p { margin: 0 }")); + declarations.push(text("* { margin: 0; padding: 0 }")); declarations.push(text( "button { border: none; cursor: pointer; outline: none }", )); - for declaration in self.styles.values() { + for declaration in self.rules.values() { declarations.push(text(*declaration)); } @@ -139,7 +139,27 @@ pub fn length(length: Length) -> String { match length { Length::Shrink => String::from("auto"), Length::Units(px) => format!("{}px", px), - Length::Fill => String::from("100%"), + Length::Fill | Length::FillPortion(_) => String::from("100%"), + } +} + +/// Returns the style value for the given maximum length in units. +pub fn max_length(units: u32) -> String { + use std::u32; + + if units == u32::MAX { + String::from("initial") + } else { + format!("{}px", units) + } +} + +/// Returns the style value for the given minimum length in units. +pub fn min_length(units: u32) -> String { + if units == 0 { + String::from("initial") + } else { + format!("{}px", units) } } @@ -150,6 +170,15 @@ pub fn color(Color { r, g, b, a }: Color) -> String { format!("rgba({}, {}, {}, {})", 255.0 * r, 255.0 * g, 255.0 * b, a) } +/// Returns the style value for the given [`Background`]. +/// +/// [`Background`]: ../struct.Background.html +pub fn background(background: Background) -> String { + match background { + Background::Color(c) => color(c), + } +} + /// Returns the style value for the given [`Align`]. /// /// [`Align`]: ../enum.Align.html diff --git a/web/src/element.rs b/web/src/element.rs index 85fa7c34..93e73713 100644 --- a/web/src/element.rs +++ b/web/src/element.rs @@ -1,4 +1,4 @@ -use crate::{style, Bus, Color, Widget}; +use crate::{Bus, Color, Css, Widget}; use dodrio::bumpalo; use std::rc::Rc; @@ -38,8 +38,8 @@ impl<'a, Message> Element<'a, Message> { /// [`Element`]: struct.Element.html pub fn map<F, B>(self, f: F) -> Element<'a, B> where - Message: 'static + Clone, - B: 'static + Clone, + Message: 'static, + B: 'static, F: 'static + Fn(Message) -> B, { Element { @@ -57,7 +57,7 @@ impl<'a, Message> Element<'a, Message> { &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { self.widget.node(bump, bus, style_sheet) } @@ -82,14 +82,14 @@ impl<'a, A, B> Map<'a, A, B> { impl<'a, A, B> Widget<B> for Map<'a, A, B> where - A: 'static + Clone, - B: 'static + Clone, + A: 'static, + B: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<B>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { self.widget .node(bump, &bus.map(self.mapper.clone()), style_sheet) diff --git a/web/src/hasher.rs b/web/src/hasher.rs new file mode 100644 index 00000000..1a28a2f9 --- /dev/null +++ b/web/src/hasher.rs @@ -0,0 +1,21 @@ +use std::collections::hash_map::DefaultHasher; + +/// The hasher used to compare subscriptions. +#[derive(Debug)] +pub struct Hasher(DefaultHasher); + +impl Default for Hasher { + fn default() -> Self { + Hasher(DefaultHasher::default()) + } +} + +impl core::hash::Hasher for Hasher { + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes) + } + + fn finish(&self) -> u64 { + self.0.finish() + } +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 8239ffc9..258ad9e7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -54,27 +54,37 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] use dodrio::bumpalo; use std::{cell::RefCell, rc::Rc}; mod bus; mod element; +mod hasher; -pub mod style; +pub mod css; +pub mod subscription; pub mod widget; pub use bus::Bus; +pub use css::Css; pub use dodrio; pub use element::Element; +pub use hasher::Hasher; pub use iced_core::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - VerticalAlignment, + Align, Background, Color, Font, HorizontalAlignment, Length, Point, Size, + Vector, VerticalAlignment, }; -pub use style::Style; +pub use iced_futures::{executor, futures, Command}; +pub use subscription::Subscription; + +#[doc(no_inline)] pub use widget::*; +#[doc(no_inline)] +pub use executor::Executor; + /// An interactive web application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -87,7 +97,15 @@ pub trait Application { /// The type of __messages__ your [`Application`] will produce. /// /// [`Application`]: trait.Application.html - type Message: Clone; + type Message: Send; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [`executor::WasmBindgen`] can be a good choice for the Web. + /// + /// [`Executor`]: trait.Executor.html + /// [`executor::Default`]: executor/struct.Default.html + type Executor: Executor; /// Initializes the [`Application`]. /// @@ -130,6 +148,20 @@ pub trait Application { /// [`Application`]: trait.Application.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + /// Runs the [`Application`]. /// /// [`Application`]: trait.Application.html @@ -137,71 +169,66 @@ pub trait Application { where Self: 'static + Sized, { - let (app, command) = Self::new(); - let mut instance = Instance::new(app); + use futures::stream::StreamExt; - instance.spawn(command); + let (app, command) = Self::new(); let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - document.set_title(&instance.title); - let body = document.body().unwrap(); - let vdom = dodrio::Vdom::new(&body, instance); - vdom.forget(); - } -} + let mut title = app.title(); + document.set_title(&title); -#[derive(Clone)] -struct Instance<Message> { - title: String, - ui: Rc<RefCell<Box<dyn Application<Message = Message>>>>, -} + let (sender, receiver) = + iced_futures::futures::channel::mpsc::unbounded(); -impl<Message> Instance<Message> -where - Message: 'static + Clone, -{ - fn new(ui: impl Application<Message = Message> + 'static) -> Self { - Self { - title: ui.title(), - ui: Rc::new(RefCell::new(Box::new(ui))), - } - } + let mut runtime = iced_futures::Runtime::new( + Self::Executor::new().expect("Create executor"), + sender.clone(), + ); + runtime.spawn(command); - fn update(&mut self, message: Message) { - let command = self.ui.borrow_mut().update(message); - let title = self.ui.borrow().title(); + let application = Rc::new(RefCell::new(app)); - self.spawn(command); + let instance = Instance { + application: application.clone(), + bus: Bus::new(sender), + }; - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); + let vdom = dodrio::Vdom::new(&body, instance); - if self.title != title { - document.set_title(&title); + let event_loop = receiver.for_each(move |message| { + let command = application.borrow_mut().update(message); + let subscription = application.borrow().subscription(); + let new_title = application.borrow().title(); - self.title = title; - } - } + runtime.spawn(command); + runtime.track(subscription); + + if title != new_title { + document.set_title(&new_title); - fn spawn(&mut self, command: Command<Message>) { - use futures::FutureExt; + title = new_title; + } - for future in command.futures() { - let mut instance = self.clone(); - let future = future.map(move |message| instance.update(message)); + vdom.weak().schedule_render(); - wasm_bindgen_futures::spawn_local(future); - } + futures::future::ready(()) + }); + + wasm_bindgen_futures::spawn_local(event_loop); } } -impl<Message> dodrio::Render for Instance<Message> +struct Instance<A: Application> { + application: Rc<RefCell<A>>, + bus: Bus<A::Message>, +} + +impl<A> dodrio::Render for Instance<A> where - Message: 'static + Clone, + A: Application, { fn render<'a, 'bump>( &'a self, @@ -212,15 +239,15 @@ where { use dodrio::builder::*; - let mut ui = self.ui.borrow_mut(); + let mut ui = self.application.borrow_mut(); let element = ui.view(); - let mut style_sheet = style::Sheet::new(); + let mut css = Css::new(); - let node = element.widget.node(bump, &Bus::new(), &mut style_sheet); + let node = element.widget.node(bump, &self.bus, &mut css); div(bump) .attr("style", "width: 100%; height: 100%") - .children(vec![style_sheet.node(bump), node]) + .children(vec![css.node(bump), node]) .finish() } } diff --git a/web/src/subscription.rs b/web/src/subscription.rs new file mode 100644 index 00000000..6b8415c0 --- /dev/null +++ b/web/src/subscription.rs @@ -0,0 +1,19 @@ +//! Listen to external events in your application. +use crate::Hasher; + +/// A request to listen to external events. +/// +/// Besides performing async actions on demand with [`Command`], most +/// applications also need to listen to external events passively. +/// +/// A [`Subscription`] is normally provided to some runtime, like a [`Command`], +/// and it will generate events as long as the user keeps requesting it. +/// +/// For instance, you can use a [`Subscription`] to listen to a WebSocket +/// connection, keyboard presses, mouse events, time ticks, etc. +/// +/// [`Command`]: ../struct.Command.html +/// [`Subscription`]: struct.Subscription.html +pub type Subscription<T> = iced_futures::Subscription<Hasher, (), T>; + +pub use iced_futures::subscription::Recipe; diff --git a/web/src/widget.rs b/web/src/widget.rs index b0e16692..025cf22f 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -14,20 +14,22 @@ //! ``` //! //! [`Widget`]: trait.Widget.html -use crate::{style, Bus}; +use crate::{Bus, Css}; use dodrio::bumpalo; pub mod button; +pub mod checkbox; +pub mod container; +pub mod image; +pub mod progress_bar; +pub mod radio; pub mod scrollable; pub mod slider; pub mod text_input; -mod checkbox; mod column; -mod container; -mod image; -mod radio; mod row; +mod space; mod text; #[doc(no_inline)] @@ -45,8 +47,10 @@ pub use checkbox::Checkbox; pub use column::Column; pub use container::Container; pub use image::Image; +pub use progress_bar::ProgressBar; pub use radio::Radio; pub use row::Row; +pub use space::Space; /// A component that displays information and allows interaction. /// @@ -62,6 +66,6 @@ pub trait Widget<Message> { &self, bump: &'b bumpalo::Bump, _bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b>; } diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs index 889c0ab1..3a5afe60 100644 --- a/web/src/widget/button.rs +++ b/web/src/widget/button.rs @@ -4,7 +4,9 @@ //! //! [`Button`]: struct.Button.html //! [`State`]: struct.State.html -use crate::{style, Background, Bus, Element, Length, Style, Widget}; +use crate::{css, Background, Bus, Css, Element, Length, Widget}; + +pub use iced_style::button::{Style, StyleSheet}; use dodrio::bumpalo; @@ -26,10 +28,11 @@ pub struct Button<'a, Message> { content: Element<'a, Message>, on_press: Option<Message>, width: Length, + height: Length, min_width: u32, + min_height: u32, padding: u16, - background: Option<Background>, - border_radius: u16, + style: Box<dyn StyleSheet>, } impl<'a, Message> Button<'a, Message> { @@ -46,10 +49,11 @@ impl<'a, Message> Button<'a, Message> { content: content.into(), on_press: None, width: Length::Shrink, + height: Length::Shrink, min_width: 0, - padding: 0, - background: None, - border_radius: 0, + min_height: 0, + padding: 5, + style: Default::default(), } } @@ -61,6 +65,14 @@ impl<'a, Message> Button<'a, Message> { self } + /// Sets the height of the [`Button`]. + /// + /// [`Button`]: struct.Button.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + /// Sets the minimum width of the [`Button`]. /// /// [`Button`]: struct.Button.html @@ -69,28 +81,27 @@ impl<'a, Message> Button<'a, Message> { self } - /// Sets the padding of the [`Button`]. + /// Sets the minimum height of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + pub fn min_height(mut self, min_height: u32) -> Self { + self.min_height = min_height; self } - /// Sets the [`Background`] of the [`Button`]. + /// Sets the padding of the [`Button`]. /// /// [`Button`]: struct.Button.html - /// [`Background`]: ../../struct.Background.html - pub fn background(mut self, background: Background) -> Self { - self.background = Some(background); + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; self } - /// Sets the border radius of the [`Button`]. + /// Sets the style of the [`Button`]. /// /// [`Button`]: struct.Button.html - pub fn border_radius(mut self, border_radius: u16) -> Self { - self.border_radius = border_radius; + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } @@ -126,17 +137,20 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + // TODO: State-based styling + let style = self.style.active(); + let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); - let background = match self.background { + let background = match style.background { None => String::from("none"), Some(background) => match background { - Background::Color(color) => style::color(color), + Background::Color(color) => css::color(color), }, }; @@ -149,10 +163,12 @@ where "style", bumpalo::format!( in bump, - "background: {}; border-radius: {}px; min-width: {}px", + "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}", background, - self.border_radius, - self.min_width + style.border_radius, + css::length(self.width), + css::min_length(self.min_width), + css::color(style.text_color) ) .into_bump_str(), ) @@ -161,15 +177,11 @@ where if let Some(on_press) = self.on_press.clone() { let event_bus = bus.clone(); - node = node.on("click", move |root, vdom, _event| { - event_bus.publish(on_press.clone(), root); - - vdom.schedule_render(); + node = node.on("click", move |_root, _vdom, _event| { + event_bus.publish(on_press.clone()); }); } - // TODO: Complete styling - node.finish() } } diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs index b81a0d52..0657ccfb 100644 --- a/web/src/widget/checkbox.rs +++ b/web/src/widget/checkbox.rs @@ -1,6 +1,10 @@ -use crate::{style, Bus, Color, Element, Widget}; +//! Show toggle controls using checkboxes. +use crate::{css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::checkbox::{Style, StyleSheet}; use dodrio::bumpalo; +use std::rc::Rc; /// A box that can be checked. /// @@ -22,9 +26,10 @@ use dodrio::bumpalo; #[allow(missing_debug_implementations)] pub struct Checkbox<Message> { is_checked: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Rc<dyn Fn(bool) -> Message>, label: String, - label_color: Option<Color>, + width: Length, + style: Box<dyn StyleSheet>, } impl<Message> Checkbox<Message> { @@ -44,51 +49,77 @@ impl<Message> Checkbox<Message> { { Checkbox { is_checked, - on_toggle: Box::new(f), + on_toggle: Rc::new(f), label: String::from(label), - label_color: None, + width: Length::Shrink, + style: Default::default(), } } - /// Sets the color of the label of the [`Checkbox`]. + /// Sets the width of the [`Checkbox`]. + /// + /// [`Checkbox`]: struct.Checkbox.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the style of the [`Checkbox`]. /// /// [`Checkbox`]: struct.Checkbox.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } } impl<Message> Widget<Message> for Checkbox<Message> where - Message: 'static + Clone, + Message: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; let checkbox_label = bumpalo::format!(in bump, "{}", self.label); let event_bus = bus.clone(); - let msg = (self.on_toggle)(!self.is_checked); + let on_toggle = self.on_toggle.clone(); + let is_checked = self.is_checked; + + let row_class = style_sheet.insert(bump, css::Rule::Row); + + let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); - // TODO: Complete styling label(bump) + .attr( + "class", + bumpalo::format!(in bump, "{} {}", row_class, spacing_class) + .into_bump_str(), + ) + .attr( + "style", + bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) + .into_bump_str(), + ) .children(vec![ + // TODO: Checkbox styling input(bump) .attr("type", "checkbox") .bool_attr("checked", self.is_checked) - .on("click", move |root, vdom, _event| { - event_bus.publish(msg.clone(), root); + .on("click", move |_root, vdom, _event| { + let msg = on_toggle(!is_checked); + event_bus.publish(msg); vdom.schedule_render(); }) .finish(), - text(checkbox_label.into_bump_str()), + span(bump).children(vec![ + text(checkbox_label.into_bump_str())]).finish(), ]) .finish() } @@ -96,7 +127,7 @@ where impl<'a, Message> From<Checkbox<Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(checkbox: Checkbox<Message>) -> Element<'a, Message> { Element::new(checkbox) diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs index cc850f5f..6454ffba 100644 --- a/web/src/widget/column.rs +++ b/web/src/widget/column.rs @@ -1,4 +1,4 @@ -use crate::{style, Align, Bus, Element, Length, Style, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Widget}; use dodrio::bumpalo; use std::u32; @@ -112,7 +112,7 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -122,16 +122,13 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { .map(|element| element.widget.node(bump, publish, style_sheet)) .collect(); - let column_class = style_sheet.insert(bump, Style::Column); + let column_class = style_sheet.insert(bump, css::Rule::Column); let spacing_class = - style_sheet.insert(bump, Style::Spacing(self.spacing)); + style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); - - let width = style::length(self.width); - let height = style::length(self.height); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -142,10 +139,12 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px", - width, - height, - self.max_width + "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::max_length(self.max_height), + css::align(self.align_items) ).into_bump_str() ) .children(children) diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs index 25e4ebf8..8e4318f9 100644 --- a/web/src/widget/container.rs +++ b/web/src/widget/container.rs @@ -1,4 +1,7 @@ -use crate::{bumpalo, style, Align, Bus, Element, Length, Style, Widget}; +//! Decorate content and apply alignment. +use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Widget}; + +pub use iced_style::container::{Style, StyleSheet}; /// An element decorating some content. /// @@ -11,6 +14,7 @@ pub struct Container<'a, Message> { max_height: u32, horizontal_alignment: Align, vertical_alignment: Align, + style_sheet: Box<dyn StyleSheet>, content: Element<'a, Message>, } @@ -31,6 +35,7 @@ impl<'a, Message> Container<'a, Message> { max_height: u32::MAX, horizontal_alignment: Align::Start, vertical_alignment: Align::Start, + style_sheet: Default::default(), content: content.into(), } } @@ -84,6 +89,14 @@ impl<'a, Message> Container<'a, Message> { self } + + /// Sets the style of the [`Container`]. + /// + /// [`Container`]: struct.Container.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget<Message> for Container<'a, Message> @@ -94,17 +107,13 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let column_class = style_sheet.insert(bump, Style::Column); + let column_class = style_sheet.insert(bump, css::Rule::Column); - let width = style::length(self.width); - let height = style::length(self.height); - - let align_items = style::align(self.horizontal_alignment); - let justify_content = style::align(self.vertical_alignment); + let style = self.style_sheet.style(); let node = div(bump) .attr( @@ -115,12 +124,17 @@ where "style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px; align-items: {}; justify-content: {}", - width, - height, - self.max_width, - align_items, - justify_content + "width: {}; height: {}; max-width: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::align(self.horizontal_alignment), + css::align(self.vertical_alignment), + style.background.map(css::background).unwrap_or(String::from("initial")), + style.text_color.map(css::color).unwrap_or(String::from("inherit")), + style.border_width, + css::color(style.border_color), + style.border_radius ) .into_bump_str(), ) @@ -134,7 +148,7 @@ where impl<'a, Message> From<Container<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(container: Container<'a, Message>) -> Element<'a, Message> { Element::new(container) diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs index ed8b7ecf..029ab352 100644 --- a/web/src/widget/image.rs +++ b/web/src/widget/image.rs @@ -1,6 +1,12 @@ -use crate::{style, Bus, Element, Length, Widget}; +//! Display images in your user interface. +use crate::{Bus, Css, Element, Hasher, Length, Widget}; use dodrio::bumpalo; +use std::{ + hash::{Hash, Hasher as _}, + path::PathBuf, + sync::Arc, +}; /// A frame that displays an image while keeping aspect ratio. /// @@ -14,7 +20,7 @@ use dodrio::bumpalo; #[derive(Debug)] pub struct Image { /// The image path - pub path: String, + pub handle: Handle, /// The width of the image pub width: Length, @@ -27,9 +33,9 @@ impl Image { /// Creates a new [`Image`] with the given path. /// /// [`Image`]: struct.Image.html - pub fn new<T: Into<String>>(path: T) -> Self { + pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { - path: path.into(), + handle: handle.into(), width: Length::Shrink, height: Length::Shrink, } @@ -57,17 +63,19 @@ impl<Message> Widget<Message> for Image { &self, bump: &'b bumpalo::Bump, _bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let src = bumpalo::format!(in bump, "{}", self.path); + let src = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() { + Data::Path(path) => path.to_str().unwrap_or("") + }); let mut image = img(bump).attr("src", src.into_bump_str()); match self.width { Length::Shrink => {} - Length::Fill => { + Length::Fill | Length::FillPortion(_) => { image = image.attr("width", "100%"); } Length::Units(px) => { @@ -89,3 +97,74 @@ impl<'a, Message> From<Image> for Element<'a, Message> { Element::new(image) } } + +/// An [`Image`] handle. +/// +/// [`Image`]: struct.Image.html +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc<Data>, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// [`Handle`]: struct.Handle.html + pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data: Arc::new(data), + } + } + + /// Returns the unique identifier of the [`Handle`]. + /// + /// [`Handle`]: struct.Handle.html + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the image [`Data`]. + /// + /// [`Data`]: enum.Data.html + pub fn data(&self) -> &Data { + &self.data + } +} + +impl From<String> for Handle { + fn from(path: String) -> Handle { + Handle::from_path(path) + } +} + +impl From<&str> for Handle { + fn from(path: &str) -> Handle { + Handle::from_path(path) + } +} + +/// The data of an [`Image`]. +/// +/// [`Image`]: struct.Image.html +#[derive(Clone, Hash)] +pub enum Data { + /// A remote image + Path(PathBuf), +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({:?})", path), + } + } +} diff --git a/web/src/widget/progress_bar.rs b/web/src/widget/progress_bar.rs new file mode 100644 index 00000000..856203c0 --- /dev/null +++ b/web/src/widget/progress_bar.rs @@ -0,0 +1,124 @@ +//! Provide progress feedback to your users. +use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::progress_bar::{Style, StyleSheet}; + +use std::ops::RangeInclusive; + +/// A bar that displays progress. +/// +/// # Example +/// ``` +/// use iced_web::ProgressBar; +/// +/// let value = 50.0; +/// +/// ProgressBar::new(0.0..=100.0, value); +/// ``` +/// +///  +#[allow(missing_debug_implementations)] +pub struct ProgressBar { + range: RangeInclusive<f32>, + value: f32, + width: Length, + height: Option<Length>, + style: Box<dyn StyleSheet>, +} + +impl ProgressBar { + /// Creates a new [`ProgressBar`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`ProgressBar`] + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { + ProgressBar { + value: value.max(*range.start()).min(*range.end()), + range, + width: Length::Fill, + height: None, + style: Default::default(), + } + } + + /// Sets the width of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn height(mut self, height: Length) -> Self { + self.height = Some(height); + self + } + + /// Sets the style of the [`ProgressBar`]. + /// + /// [`ProgressBar`]: struct.ProgressBar.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message> Widget<Message> for ProgressBar { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _bus: &Bus<Message>, + _style_sheet: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let (range_start, range_end) = self.range.clone().into_inner(); + let amount_filled = + (self.value - range_start) / (range_end - range_start).max(1.0); + + let style = self.style.style(); + + let bar = div(bump) + .attr( + "style", + bumpalo::format!( + in bump, + "width: {}%; height: 100%; background: {}", + amount_filled * 100.0, + css::background(style.bar) + ) + .into_bump_str(), + ) + .finish(); + + let node = div(bump).attr( + "style", + bumpalo::format!( + in bump, + "width: {}; height: {}; background: {}; border-radius: {}px; overflow: hidden;", + css::length(self.width), + css::length(self.height.unwrap_or(Length::Units(30))), + css::background(style.background), + style.border_radius + ) + .into_bump_str(), + ).children(vec![bar]); + + node.finish() + } +} + +impl<'a, Message> From<ProgressBar> for Element<'a, Message> +where + Message: 'static, +{ + fn from(container: ProgressBar) -> Element<'a, Message> { + Element::new(container) + } +} diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs index 4e7d02b8..e00e26db 100644 --- a/web/src/widget/radio.rs +++ b/web/src/widget/radio.rs @@ -1,4 +1,7 @@ -use crate::{style, Bus, Color, Element, Widget}; +//! Create choices using radio buttons. +use crate::{Bus, Css, Element, Widget}; + +pub use iced_style::radio::{Style, StyleSheet}; use dodrio::bumpalo; @@ -32,7 +35,7 @@ pub struct Radio<Message> { is_selected: bool, on_click: Message, label: String, - label_color: Option<Color>, + style: Box<dyn StyleSheet>, } impl<Message> Radio<Message> { @@ -55,15 +58,15 @@ impl<Message> Radio<Message> { is_selected: Some(value) == selected, on_click: f(value), label: String::from(label), - label_color: None, + style: Default::default(), } } - /// Sets the `Color` of the label of the [`Radio`]. + /// Sets the style of the [`Radio`] button. /// /// [`Radio`]: struct.Radio.html - pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self { - self.label_color = Some(color.into()); + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); self } } @@ -76,7 +79,7 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -93,10 +96,8 @@ where .attr("type", "radio") .attr("style", "margin-right: 10px") .bool_attr("checked", self.is_selected) - .on("click", move |root, vdom, _event| { - event_bus.publish(on_click.clone(), root); - - vdom.schedule_render(); + .on("click", move |_root, _vdom, _event| { + event_bus.publish(on_click.clone()); }) .finish(), text(radio_label.into_bump_str()), diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs index e47478be..02035113 100644 --- a/web/src/widget/row.rs +++ b/web/src/widget/row.rs @@ -1,4 +1,4 @@ -use crate::{style, Align, Bus, Element, Length, Style, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Widget}; use dodrio::bumpalo; use std::u32; @@ -113,7 +113,7 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { &self, bump: &'b bumpalo::Bump, publish: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; @@ -123,16 +123,13 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { .map(|element| element.widget.node(bump, publish, style_sheet)) .collect(); - let row_class = style_sheet.insert(bump, Style::Row); + let row_class = style_sheet.insert(bump, css::Rule::Row); let spacing_class = - style_sheet.insert(bump, Style::Spacing(self.spacing)); + style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); - - let width = style::length(self.width); - let height = style::length(self.height); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); // TODO: Complete styling div(bump) @@ -143,10 +140,12 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}px", - width, - height, - self.max_width + "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + css::length(self.width), + css::length(self.height), + css::max_length(self.max_width), + css::max_length(self.max_height), + css::align(self.align_items) ).into_bump_str() ) .children(children) diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs index 710bb70a..07b38aad 100644 --- a/web/src/widget/scrollable.rs +++ b/web/src/widget/scrollable.rs @@ -1,5 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. -use crate::{bumpalo, style, Align, Bus, Column, Element, Length, Widget}; +use crate::{bumpalo, css, Align, Bus, Column, Css, Element, Length, Widget}; + +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -9,6 +11,7 @@ pub struct Scrollable<'a, Message> { height: Length, max_height: u32, content: Column<'a, Message>, + style: Box<dyn StyleSheet>, } impl<'a, Message> Scrollable<'a, Message> { @@ -24,6 +27,7 @@ impl<'a, Message> Scrollable<'a, Message> { height: Length::Shrink, max_height: u32::MAX, content: Column::new(), + style: Default::default(), } } @@ -85,6 +89,14 @@ impl<'a, Message> Scrollable<'a, Message> { self } + /// Sets the style of the [`Scrollable`] . + /// + /// [`Scrollable`]: struct.Scrollable.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } + /// Adds an element to the [`Scrollable`]. /// /// [`Scrollable`]: struct.Scrollable.html @@ -105,12 +117,14 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; - let width = style::length(self.width); - let height = style::length(self.height); + let width = css::length(self.width); + let height = css::length(self.height); + + // TODO: Scrollbar styling let node = div(bump) .attr( @@ -126,15 +140,13 @@ where ) .children(vec![self.content.node(bump, bus, style_sheet)]); - // TODO: Complete styling - node.finish() } } impl<'a, Message> From<Scrollable<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(scrollable: Scrollable<'a, Message>) -> Element<'a, Message> { Element::new(scrollable) diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 5b203e07..5aa6439e 100644 --- a/web/src/widget/slider.rs +++ b/web/src/widget/slider.rs @@ -4,7 +4,9 @@ //! //! [`Slider`]: struct.Slider.html //! [`State`]: struct.State.html -use crate::{style, Bus, Element, Length, Widget}; +use crate::{Bus, Css, Element, Length, Widget}; + +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; use dodrio::bumpalo; use std::{ops::RangeInclusive, rc::Rc}; @@ -38,6 +40,7 @@ pub struct Slider<'a, Message> { value: f32, on_change: Rc<Box<dyn Fn(f32) -> Message>>, width: Length, + style: Box<dyn StyleSheet>, } impl<'a, Message> Slider<'a, Message> { @@ -68,6 +71,7 @@ impl<'a, Message> Slider<'a, Message> { range, on_change: Rc::new(Box::new(on_change)), width: Length::Fill, + style: Default::default(), } } @@ -78,17 +82,25 @@ impl<'a, Message> Slider<'a, Message> { self.width = width; self } + + /// Sets the style of the [`Slider`]. + /// + /// [`Slider`]: struct.Slider.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } } impl<'a, Message> Widget<Message> for Slider<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn node<'b>( &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; @@ -103,7 +115,7 @@ where let event_bus = bus.clone(); // TODO: Make `step` configurable - // TODO: Complete styling + // TODO: Styling input(bump) .attr("type", "range") .attr("step", "0.01") @@ -111,7 +123,7 @@ where .attr("max", max.into_bump_str()) .attr("value", value.into_bump_str()) .attr("style", "width: 100%") - .on("input", move |root, vdom, event| { + .on("input", move |_root, _vdom, event| { let slider = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() }) { @@ -120,8 +132,7 @@ where }; if let Ok(value) = slider.value().parse::<f32>() { - event_bus.publish(on_change(value), root); - vdom.schedule_render(); + event_bus.publish(on_change(value)); } }) .finish() @@ -130,7 +141,7 @@ where impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message> where - Message: 'static + Clone, + Message: 'static, { fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { Element::new(slider) diff --git a/web/src/widget/space.rs b/web/src/widget/space.rs new file mode 100644 index 00000000..4ce52595 --- /dev/null +++ b/web/src/widget/space.rs @@ -0,0 +1,69 @@ +use crate::{css, Bus, Css, Element, Length, Widget}; +use dodrio::bumpalo; + +/// An amount of empty space. +/// +/// It can be useful if you want to fill some space with nothing. +#[derive(Debug)] +pub struct Space { + width: Length, + height: Length, +} + +impl Space { + /// Creates an amount of empty [`Space`] with the given width and height. + /// + /// [`Space`]: struct.Space.html + pub fn new(width: Length, height: Length) -> Self { + Space { width, height } + } + + /// Creates an amount of horizontal [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_width(width: Length) -> Self { + Space { + width, + height: Length::Shrink, + } + } + + /// Creates an amount of vertical [`Space`]. + /// + /// [`Space`]: struct.Space.html + pub fn with_height(height: Length) -> Self { + Space { + width: Length::Shrink, + height, + } + } +} + +impl<'a, Message> Widget<Message> for Space { + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + _publish: &Bus<Message>, + _css: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + + let width = css::length(self.width); + let height = css::length(self.height); + + let style = bumpalo::format!( + in bump, + "width: {}; height: {};", + width, + height + ); + + div(bump).attr("style", style.into_bump_str()).finish() + } +} + +impl<'a, Message> From<Space> for Element<'a, Message> { + fn from(space: Space) -> Element<'a, Message> { + Element::new(space) + } +} diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs index 6194a12e..3ec565a8 100644 --- a/web/src/widget/text.rs +++ b/web/src/widget/text.rs @@ -1,5 +1,5 @@ use crate::{ - style, Bus, Color, Element, Font, HorizontalAlignment, Length, + css, Bus, Color, Css, Element, Font, HorizontalAlignment, Length, VerticalAlignment, Widget, }; use dodrio::bumpalo; @@ -36,7 +36,7 @@ impl Text { size: None, color: None, font: Font::Default, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, horizontal_alignment: HorizontalAlignment::Left, vertical_alignment: VerticalAlignment::Top, @@ -112,12 +112,18 @@ impl<'a, Message> Widget<Message> for Text { &self, bump: &'b bumpalo::Bump, _publish: &Bus<Message>, - _style_sheet: &mut style::Sheet<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; let content = bumpalo::format!(in bump, "{}", self.content); - let color = style::color(self.color.unwrap_or(Color::BLACK)); + let color = self + .color + .map(css::color) + .unwrap_or(String::from("inherit")); + + let width = css::length(self.width); + let height = css::length(self.height); let text_align = match self.horizontal_alignment { HorizontalAlignment::Left => "left", @@ -127,10 +133,16 @@ impl<'a, Message> Widget<Message> for Text { let style = bumpalo::format!( in bump, - "font-size: {}px; color: {}; text-align: {}", + "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}", + width, + height, self.size.unwrap_or(20), color, - text_align + text_align, + match self.font { + Font::Default => "inherit", + Font::External { name, .. } => name, + } ); // TODO: Complete styling diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs index d6357512..3fa458bd 100644 --- a/web/src/widget/text_input.rs +++ b/web/src/widget/text_input.rs @@ -4,8 +4,11 @@ //! //! [`TextInput`]: struct.TextInput.html //! [`State`]: struct.State.html -use crate::{bumpalo, style, Bus, Element, Length, Style, Widget}; -use std::rc::Rc; +use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::text_input::{Style, StyleSheet}; + +use std::{rc::Rc, u32}; /// A field that can be filled with text. /// @@ -32,12 +35,14 @@ pub struct TextInput<'a, Message> { _state: &'a mut State, placeholder: String, value: String, + is_secure: bool, width: Length, - max_width: Length, + max_width: u32, padding: u16, size: Option<u16>, on_change: Rc<Box<dyn Fn(String) -> Message>>, on_submit: Option<Message>, + style_sheet: Box<dyn StyleSheet>, } impl<'a, Message> TextInput<'a, Message> { @@ -64,15 +69,25 @@ impl<'a, Message> TextInput<'a, Message> { _state: state, placeholder: String::from(placeholder), value: String::from(value), + is_secure: false, width: Length::Fill, - max_width: Length::Shrink, + max_width: u32::MAX, padding: 0, size: None, on_change: Rc::new(Box::new(on_change)), on_submit: None, + style_sheet: Default::default(), } } + /// Converts the [`TextInput`] into a secure password input. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn password(mut self) -> Self { + self.is_secure = true; + self + } + /// Sets the width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html @@ -84,7 +99,7 @@ impl<'a, Message> TextInput<'a, Message> { /// Sets the maximum width of the [`TextInput`]. /// /// [`TextInput`]: struct.TextInput.html - pub fn max_width(mut self, max_width: Length) -> Self { + pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } @@ -113,6 +128,14 @@ impl<'a, Message> TextInput<'a, Message> { self.on_submit = Some(message); self } + + /// Sets the style of the [`TextInput`]. + /// + /// [`TextInput`]: struct.TextInput.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style_sheet = style.into(); + self + } } impl<'a, Message> Widget<Message> for TextInput<'a, Message> @@ -123,16 +146,19 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut style::Sheet<'b>, + style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; let padding_class = - style_sheet.insert(bump, Style::Padding(self.padding)); + style_sheet.insert(bump, css::Rule::Padding(self.padding)); let on_change = self.on_change.clone(); - let event_bus = bus.clone(); + let on_submit = self.on_submit.clone(); + let input_event_bus = bus.clone(); + let submit_event_bus = bus.clone(); + let style = self.style_sheet.active(); input(bump) .attr( @@ -143,8 +169,15 @@ where "style", bumpalo::format!( in bump, - "font-size: {}px", - self.size.unwrap_or(20) + "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}", + css::length(self.width), + css::max_length(self.max_width), + self.size.unwrap_or(20), + css::background(style.background), + style.border_width, + css::color(style.border_color), + style.border_radius, + css::color(self.style_sheet.value_color()) ) .into_bump_str(), ) @@ -157,7 +190,11 @@ where "value", bumpalo::format!(in bump, "{}", self.value).into_bump_str(), ) - .on("input", move |root, vdom, event| { + .attr( + "type", + bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(), + ) + .on("input", move |_root, _vdom, event| { let text_input = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() }) { @@ -165,8 +202,17 @@ where Some(text_input) => text_input, }; - event_bus.publish(on_change(text_input.value()), root); - vdom.schedule_render(); + input_event_bus.publish(on_change(text_input.value())); + }) + .on("keypress", move |_root, _vdom, event| { + if let Some(on_submit) = on_submit.clone() { + let event = event.unchecked_into::<web_sys::KeyboardEvent>(); + + match event.key_code() { + 13 => { submit_event_bus.publish(on_submit); } + _ => {} + } + } }) .finish() } @@ -194,4 +240,12 @@ impl State { pub fn new() -> Self { Self::default() } + + /// Creates a new [`State`], representing a focused [`TextInput`]. + /// + /// [`State`]: struct.State.html + pub fn focused() -> Self { + // TODO + Self::default() + } } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index f008a99c..6c75af7b 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -4,16 +4,34 @@ version = "0.1.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A wgpu renderer for Iced" -license = "MIT" +license = "MIT AND OFL-1.1" repository = "https://github.com/hecrj/iced" +[features] +svg = ["resvg"] +canvas = ["lyon"] + [dependencies] iced_native = { version = "0.1.0", path = "../native" } +iced_style = { version = "0.1.0-alpha", path = "../style" } wgpu = "0.4" +wgpu_glyph = "0.7" glyph_brush = "0.6" -wgpu_glyph = "0.6" raw-window-handle = "0.3" -image = "0.22" glam = "0.8" font-kit = "0.4" log = "0.4" +guillotiere = "0.4" + +[dependencies.image] +version = "0.22" +optional = true + +[dependencies.resvg] +version = "0.8" +features = ["raqote-backend"] +optional = true + +[dependencies.lyon] +version = "0.15" +optional = true diff --git a/wgpu/fonts/Lato-Regular.ttf b/wgpu/fonts/Lato-Regular.ttf Binary files differnew file mode 100644 index 00000000..33eba8b1 --- /dev/null +++ b/wgpu/fonts/Lato-Regular.ttf diff --git a/wgpu/fonts/OFL.txt b/wgpu/fonts/OFL.txt new file mode 100644 index 00000000..dfca0da4 --- /dev/null +++ b/wgpu/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/wgpu/src/defaults.rs b/wgpu/src/defaults.rs new file mode 100644 index 00000000..11718a87 --- /dev/null +++ b/wgpu/src/defaults.rs @@ -0,0 +1,32 @@ +//! Use default styling attributes to inherit styles. +use iced_native::Color; + +/// Some default styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Defaults { + /// Text styling + pub text: Text, +} + +impl Default for Defaults { + fn default() -> Defaults { + Defaults { + text: Text::default(), + } + } +} + +/// Some default text styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Text { + /// The default color of text + pub color: Color, +} + +impl Default for Text { + fn default() -> Text { + Text { + color: Color::BLACK, + } + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 5dc972ac..d3603676 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -1,11 +1,30 @@ +mod atlas; + +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + use crate::Transformation; +use atlas::Atlas; + use iced_native::Rectangle; +use std::cell::RefCell; +use std::mem; + +#[cfg(feature = "image")] +use iced_native::image; -use std::{cell::RefCell, collections::HashMap, mem, rc::Rc}; +#[cfg(feature = "svg")] +use iced_native::svg; #[derive(Debug)] pub struct Pipeline { - cache: RefCell<HashMap<String, Memory>>, + #[cfg(feature = "image")] + raster_cache: RefCell<raster::Cache>, + #[cfg(feature = "svg")] + vector_cache: RefCell<vector::Cache>, pipeline: wgpu::RenderPipeline, uniforms: wgpu::Buffer, @@ -13,11 +32,14 @@ pub struct Pipeline { indices: wgpu::Buffer, instances: wgpu::Buffer, constants: wgpu::BindGroup, + texture: wgpu::BindGroup, + texture_version: usize, texture_layout: wgpu::BindGroupLayout, + texture_atlas: Atlas, } impl Pipeline { - pub fn new(device: &wgpu::Device) -> Self { + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -124,7 +146,7 @@ impl Pipeline { }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format, color_blend: wgpu::BlendDescriptor { src_factor: wgpu::BlendFactor::SrcAlpha, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, @@ -163,6 +185,21 @@ impl Pipeline { format: wgpu::VertexFormat::Float2, offset: 4 * 2, }, + wgpu::VertexAttributeDescriptor { + shader_location: 3, + format: wgpu::VertexFormat::Float2, + offset: 4 * 4, + }, + wgpu::VertexAttributeDescriptor { + shader_location: 4, + format: wgpu::VertexFormat::Float2, + offset: 4 * 6, + }, + wgpu::VertexAttributeDescriptor { + shader_location: 5, + format: wgpu::VertexFormat::Uint, + offset: 4 * 8, + }, ], }, ], @@ -180,12 +217,28 @@ impl Pipeline { .fill_from_slice(&QUAD_INDICES); let instances = device.create_buffer(&wgpu::BufferDescriptor { - size: mem::size_of::<Instance>() as u64, + size: mem::size_of::<Instance>() as u64 * Instance::MAX as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, }); + let texture_atlas = Atlas::new(device); + + let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &texture_atlas.view(), + ), + }], + }); + Pipeline { - cache: RefCell::new(HashMap::new()), + #[cfg(feature = "image")] + raster_cache: RefCell::new(raster::Cache::new()), + + #[cfg(feature = "svg")] + vector_cache: RefCell::new(vector::Cache::new()), pipeline, uniforms: uniforms_buffer, @@ -193,39 +246,99 @@ impl Pipeline { indices, instances, constants: constant_bind_group, + texture, + texture_version: texture_atlas.layer_count(), texture_layout, + texture_atlas, } } - pub fn dimensions(&self, path: &str) -> (u32, u32) { - self.load(path); + #[cfg(feature = "image")] + pub fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + let mut cache = self.raster_cache.borrow_mut(); + let memory = cache.load(&handle); - self.cache.borrow().get(path).unwrap().dimensions() + memory.dimensions() } - fn load(&self, path: &str) { - if !self.cache.borrow().contains_key(path) { - let memory = if let Ok(image) = image::open(path) { - Memory::Host { - image: image.to_bgra(), - } - } else { - Memory::NotFound - }; + #[cfg(feature = "svg")] + pub fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + let mut cache = self.vector_cache.borrow_mut(); + let svg = cache.load(&handle); - let _ = self.cache.borrow_mut().insert(path.to_string(), memory); - } + svg.viewport_dimensions() } pub fn draw( &mut self, device: &mut wgpu::Device, encoder: &mut wgpu::CommandEncoder, - instances: &[Image], + images: &[Image], transformation: Transformation, bounds: Rectangle<u32>, target: &wgpu::TextureView, + _scale: f32, ) { + let instances: &mut Vec<Instance> = &mut Vec::new(); + + #[cfg(feature = "image")] + let mut raster_cache = self.raster_cache.borrow_mut(); + + #[cfg(feature = "svg")] + let mut vector_cache = self.vector_cache.borrow_mut(); + + for image in images { + match &image.handle { + #[cfg(feature = "image")] + Handle::Raster(handle) => { + if let Some(atlas_entry) = raster_cache.upload( + handle, + device, + encoder, + &mut self.texture_atlas, + ) { + add_instances(image, atlas_entry, instances); + } + } + #[cfg(feature = "svg")] + Handle::Vector(handle) => { + if let Some(atlas_entry) = vector_cache.upload( + handle, + image.size, + _scale, + device, + encoder, + &mut self.texture_atlas, + ) { + add_instances(image, atlas_entry, instances); + } + } + } + } + + if instances.is_empty() { + return; + } + + let texture_version = self.texture_atlas.layer_count(); + + if self.texture_version != texture_version { + log::info!("Atlas has grown. Recreating bind group..."); + + self.texture = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.texture_atlas.view(), + ), + }], + }); + + self.texture_version = texture_version; + } + let uniforms_buffer = device .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC) .fill_from_slice(&[Uniforms { @@ -240,193 +353,94 @@ impl Pipeline { std::mem::size_of::<Uniforms>() as u64, ); - // TODO: Batch draw calls using a texture atlas - // Guillotière[1] by @nical can help us a lot here. - // - // [1]: https://github.com/nical/guillotiere - for image in instances { - self.load(&image.path); - - if let Some(texture) = self - .cache - .borrow_mut() - .get_mut(&image.path) - .unwrap() - .upload(device, encoder, &self.texture_layout) - { - let instance_buffer = device - .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC) - .fill_from_slice(&[Instance { - _position: image.position, - _scale: image.scale, - }]); - - encoder.copy_buffer_to_buffer( - &instance_buffer, - 0, - &self.instances, - 0, - mem::size_of::<Instance>() as u64, - ); - - { - let mut render_pass = encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - load_op: wgpu::LoadOp::Load, - store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }, - }, - ], - depth_stencil_attachment: None, + let instances_buffer = device + .create_buffer_mapped(instances.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&instances); + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + Instance::MAX).min(total); + let amount = end - i; + + encoder.copy_buffer_to_buffer( + &instances_buffer, + (i * std::mem::size_of::<Instance>()) as u64, + &self.instances, + 0, + (amount * std::mem::size_of::<Instance>()) as u64, + ); + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + load_op: wgpu::LoadOp::Load, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, }, - ); - - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_bind_group(1, &texture, &[]); - render_pass.set_index_buffer(&self.indices, 0); - render_pass.set_vertex_buffers( - 0, - &[(&self.vertices, 0), (&self.instances, 0)], - ); - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - render_pass.draw_indexed( - 0..QUAD_INDICES.len() as u32, - 0, - 0..1 as u32, - ); - } - } - } - } -} - -#[derive(Debug)] -enum Memory { - Host { - image: image::ImageBuffer<image::Bgra<u8>, Vec<u8>>, - }, - Device { - bind_group: Rc<wgpu::BindGroup>, - width: u32, - height: u32, - }, - NotFound, -} + ], + depth_stencil_attachment: None, + }); -impl Memory { - fn dimensions(&self) -> (u32, u32) { - match self { - Memory::Host { image } => image.dimensions(), - Memory::Device { width, height, .. } => (*width, *height), - Memory::NotFound => (1, 1), + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_index_buffer(&self.indices, 0); + render_pass.set_vertex_buffers( + 0, + &[(&self.vertices, 0), (&self.instances, 0)], + ); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.draw_indexed( + 0..QUAD_INDICES.len() as u32, + 0, + 0..amount as u32, + ); + + i += Instance::MAX; } } - fn upload( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - texture_layout: &wgpu::BindGroupLayout, - ) -> Option<Rc<wgpu::BindGroup>> { - match self { - Memory::Host { image } => { - let (width, height) = image.dimensions(); - - let extent = wgpu::Extent3d { - width, - height, - depth: 1, - }; - - let texture = device.create_texture(&wgpu::TextureDescriptor { - size: extent, - array_layer_count: 1, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - usage: wgpu::TextureUsage::COPY_DST - | wgpu::TextureUsage::SAMPLED, - }); - - let slice = image.clone().into_raw(); - - let temp_buf = device - .create_buffer_mapped( - slice.len(), - wgpu::BufferUsage::COPY_SRC, - ) - .fill_from_slice(&slice[..]); - - encoder.copy_buffer_to_texture( - wgpu::BufferCopyView { - buffer: &temp_buf, - offset: 0, - row_pitch: 4 * width as u32, - image_height: height as u32, - }, - wgpu::TextureCopyView { - texture: &texture, - array_layer: 0, - mip_level: 0, - origin: wgpu::Origin3d { - x: 0.0, - y: 0.0, - z: 0.0, - }, - }, - extent, - ); - - let bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: texture_layout, - bindings: &[wgpu::Binding { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &texture.create_default_view(), - ), - }], - }); - - let bind_group = Rc::new(bind_group); - - *self = Memory::Device { - bind_group: bind_group.clone(), - width, - height, - }; + pub fn trim_cache(&mut self) { + #[cfg(feature = "image")] + self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); - Some(bind_group) - } - Memory::Device { bind_group, .. } => Some(bind_group.clone()), - Memory::NotFound => None, - } + #[cfg(feature = "svg")] + self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); } } pub struct Image { - pub path: String, + pub handle: Handle, pub position: [f32; 2], - pub scale: [f32; 2], + pub size: [f32; 2], +} + +pub enum Handle { + #[cfg(feature = "image")] + Raster(image::Handle), + + #[cfg(feature = "svg")] + Vector(svg::Handle), } +#[repr(C)] #[derive(Clone, Copy)] pub struct Vertex { _position: [f32; 2], @@ -449,10 +463,18 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -#[derive(Clone, Copy)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] struct Instance { _position: [f32; 2], - _scale: [f32; 2], + _size: [f32; 2], + _position_in_atlas: [f32; 2], + _size_in_atlas: [f32; 2], + _layer: u32, +} + +impl Instance { + pub const MAX: usize = 1_000; } #[repr(C)] @@ -460,3 +482,67 @@ struct Instance { struct Uniforms { transform: [f32; 16], } + +fn add_instances( + image: &Image, + entry: &atlas::Entry, + instances: &mut Vec<Instance>, +) { + match entry { + atlas::Entry::Contiguous(allocation) => { + add_instance(image.position, image.size, allocation, instances); + } + atlas::Entry::Fragmented { fragments, size } => { + let scaling_x = image.size[0] / size.0 as f32; + let scaling_y = image.size[1] / size.1 as f32; + + for fragment in fragments { + let allocation = &fragment.allocation; + + let [x, y] = image.position; + let (fragment_x, fragment_y) = fragment.position; + let (fragment_width, fragment_height) = allocation.size(); + + let position = [ + x + fragment_x as f32 * scaling_x, + y + fragment_y as f32 * scaling_y, + ]; + + let size = [ + fragment_width as f32 * scaling_x, + fragment_height as f32 * scaling_y, + ]; + + add_instance(position, size, allocation, instances); + } + } + } +} + +#[inline] +fn add_instance( + position: [f32; 2], + size: [f32; 2], + allocation: &atlas::Allocation, + instances: &mut Vec<Instance>, +) { + let (x, y) = allocation.position(); + let (width, height) = allocation.size(); + let layer = allocation.layer(); + + let instance = Instance { + _position: position, + _size: size, + _position_in_atlas: [ + (x as f32 + 0.5) / atlas::SIZE as f32, + (y as f32 + 0.5) / atlas::SIZE as f32, + ], + _size_in_atlas: [ + (width as f32 - 1.0) / atlas::SIZE as f32, + (height as f32 - 1.0) / atlas::SIZE as f32, + ], + _layer: layer as u32, + }; + + instances.push(instance); +} diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs new file mode 100644 index 00000000..86a5ff49 --- /dev/null +++ b/wgpu/src/image/atlas.rs @@ -0,0 +1,361 @@ +pub mod entry; + +mod allocation; +mod allocator; +mod layer; + +pub use allocation::Allocation; +pub use entry::Entry; +pub use layer::Layer; + +use allocator::Allocator; + +pub const SIZE: u32 = 2048; + +#[derive(Debug)] +pub struct Atlas { + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + layers: Vec<Layer>, +} + +impl Atlas { + pub fn new(device: &wgpu::Device) -> Self { + let extent = wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 2, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let texture_view = texture.create_default_view(); + + Atlas { + texture, + texture_view, + layers: vec![Layer::Empty, Layer::Empty], + } + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.texture_view + } + + pub fn layer_count(&self) -> usize { + self.layers.len() + } + + pub fn upload<C>( + &mut self, + width: u32, + height: u32, + data: &[C], + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> Option<Entry> + where + C: Copy + 'static, + { + let entry = { + let current_size = self.layers.len(); + let entry = self.allocate(width, height)?; + + // We grow the internal texture after allocating if necessary + let new_layers = self.layers.len() - current_size; + self.grow(new_layers, device, encoder); + + entry + }; + + log::info!("Allocated atlas entry: {:?}", entry); + + let buffer = device + .create_buffer_mapped(data.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(data); + + match &entry { + Entry::Contiguous(allocation) => { + self.upload_allocation( + &buffer, + width, + height, + 0, + &allocation, + encoder, + ); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + let (x, y) = fragment.position; + let offset = (y * width + x) as usize * 4; + + self.upload_allocation( + &buffer, + width, + height, + offset, + &fragment.allocation, + encoder, + ); + } + } + } + + log::info!("Current atlas: {:?}", self); + + Some(entry) + } + + pub fn remove(&mut self, entry: &Entry) { + log::info!("Removing atlas entry: {:?}", entry); + + match entry { + Entry::Contiguous(allocation) => { + self.deallocate(allocation); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + self.deallocate(&fragment.allocation); + } + } + } + } + + fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> { + // Allocate one layer if texture fits perfectly + if width == SIZE && height == SIZE { + let mut empty_layers = self + .layers + .iter_mut() + .enumerate() + .filter(|(_, layer)| layer.is_empty()); + + if let Some((i, layer)) = empty_layers.next() { + *layer = Layer::Full; + + return Some(Entry::Contiguous(Allocation::Full { layer: i })); + } + + self.layers.push(Layer::Full); + + return Some(Entry::Contiguous(Allocation::Full { + layer: self.layers.len() - 1, + })); + } + + // Split big textures across multiple layers + if width > SIZE || height > SIZE { + let mut fragments = Vec::new(); + let mut y = 0; + + while y < height { + let height = std::cmp::min(height - y, SIZE); + let mut x = 0; + + while x < width { + let width = std::cmp::min(width - x, SIZE); + + let allocation = self.allocate(width, height)?; + + if let Entry::Contiguous(allocation) = allocation { + fragments.push(entry::Fragment { + position: (x, y), + allocation, + }); + } + + x += width; + } + + y += height; + } + + return Some(Entry::Fragmented { + size: (width, height), + fragments, + }); + } + + // Try allocating on an existing layer + for (i, layer) in self.layers.iter_mut().enumerate() { + match layer { + Layer::Empty => { + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + *layer = Layer::Busy(allocator); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + Layer::Busy(allocator) => { + if let Some(region) = allocator.allocate(width, height) { + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + _ => {} + } + } + + // Create new layer with atlas allocator + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + self.layers.push(Layer::Busy(allocator)); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: self.layers.len() - 1, + })); + } + + // We ran out of memory (?) + None + } + + fn deallocate(&mut self, allocation: &Allocation) { + log::info!("Deallocating atlas: {:?}", allocation); + + match allocation { + Allocation::Full { layer } => { + self.layers[*layer] = Layer::Empty; + } + Allocation::Partial { layer, region } => { + let layer = &mut self.layers[*layer]; + + if let Layer::Busy(allocator) = layer { + allocator.deallocate(region); + + if allocator.is_empty() { + *layer = Layer::Empty; + } + } + } + } + } + + fn upload_allocation( + &mut self, + buffer: &wgpu::Buffer, + image_width: u32, + image_height: u32, + offset: usize, + allocation: &Allocation, + encoder: &mut wgpu::CommandEncoder, + ) { + let (x, y) = allocation.position(); + let (width, height) = allocation.size(); + let layer = allocation.layer(); + + let extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + + encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer, + offset: offset as u64, + row_pitch: 4 * image_width, + image_height, + }, + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: layer as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: x as f32, + y: y as f32, + z: 0.0, + }, + }, + extent, + ); + } + + fn grow( + &mut self, + amount: usize, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) { + if amount == 0 { + return; + } + + let new_texture = device.create_texture(&wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + array_layer_count: self.layers.len() as u32, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let amount_to_copy = self.layers.len() - amount; + + for (i, layer) in + self.layers.iter_mut().take(amount_to_copy).enumerate() + { + if layer.is_empty() { + continue; + } + + encoder.copy_texture_to_texture( + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::TextureCopyView { + texture: &new_texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + ); + } + + self.texture = new_texture; + self.texture_view = self.texture.create_default_view(); + } +} diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs new file mode 100644 index 00000000..59b7239f --- /dev/null +++ b/wgpu/src/image/atlas/allocation.rs @@ -0,0 +1,35 @@ +use crate::image::atlas::{self, allocator}; + +#[derive(Debug)] +pub enum Allocation { + Partial { + layer: usize, + region: allocator::Region, + }, + Full { + layer: usize, + }, +} + +impl Allocation { + pub fn position(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.position(), + Allocation::Full { .. } => (0, 0), + } + } + + pub fn size(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.size(), + Allocation::Full { .. } => (atlas::SIZE, atlas::SIZE), + } + } + + pub fn layer(&self) -> usize { + match self { + Allocation::Partial { layer, .. } => *layer, + Allocation::Full { layer } => *layer, + } + } +} diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs new file mode 100644 index 00000000..7a4ff5b1 --- /dev/null +++ b/wgpu/src/image/atlas/allocator.rs @@ -0,0 +1,69 @@ +use guillotiere::{AtlasAllocator, Size}; + +pub struct Allocator { + raw: AtlasAllocator, + allocations: usize, +} + +impl Allocator { + pub fn new(size: u32) -> Allocator { + let raw = AtlasAllocator::new(Size::new(size as i32, size as i32)); + + Allocator { + raw, + allocations: 0, + } + } + + pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> { + let allocation = + self.raw.allocate(Size::new(width as i32, height as i32))?; + + self.allocations += 1; + + Some(Region { allocation }) + } + + pub fn deallocate(&mut self, region: &Region) { + self.raw.deallocate(region.allocation.id); + + self.allocations = self.allocations.saturating_sub(1); + } + + pub fn is_empty(&self) -> bool { + self.allocations == 0 + } +} + +pub struct Region { + allocation: guillotiere::Allocation, +} + +impl Region { + pub fn position(&self) -> (u32, u32) { + let rectangle = &self.allocation.rectangle; + + (rectangle.min.x as u32, rectangle.min.y as u32) + } + + pub fn size(&self) -> (u32, u32) { + let size = self.allocation.rectangle.size(); + + (size.width as u32, size.height as u32) + } +} + +impl std::fmt::Debug for Allocator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Allocator") + } +} + +impl std::fmt::Debug for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Region") + .field("id", &self.allocation.id) + .field("rectangle", &self.allocation.rectangle) + .finish() + } +} diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs new file mode 100644 index 00000000..0310fc54 --- /dev/null +++ b/wgpu/src/image/atlas/entry.rs @@ -0,0 +1,26 @@ +use crate::image::atlas; + +#[derive(Debug)] +pub enum Entry { + Contiguous(atlas::Allocation), + Fragmented { + size: (u32, u32), + fragments: Vec<Fragment>, + }, +} + +impl Entry { + #[cfg(feature = "image")] + pub fn size(&self) -> (u32, u32) { + match self { + Entry::Contiguous(allocation) => allocation.size(), + Entry::Fragmented { size, .. } => *size, + } + } +} + +#[derive(Debug)] +pub struct Fragment { + pub position: (u32, u32), + pub allocation: atlas::Allocation, +} diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs new file mode 100644 index 00000000..b1084ed9 --- /dev/null +++ b/wgpu/src/image/atlas/layer.rs @@ -0,0 +1,17 @@ +use crate::image::atlas::Allocator; + +#[derive(Debug)] +pub enum Layer { + Empty, + Busy(Allocator), + Full, +} + +impl Layer { + pub fn is_empty(&self) -> bool { + match self { + Layer::Empty => true, + _ => false, + } + } +} diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs new file mode 100644 index 00000000..4f69df8c --- /dev/null +++ b/wgpu/src/image/raster.rs @@ -0,0 +1,134 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::image; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug)] +pub enum Memory { + Host(::image::ImageBuffer<::image::Bgra<u8>, Vec<u8>>), + Device(atlas::Entry), + NotFound, + Invalid, +} + +impl Memory { + pub fn dimensions(&self) -> (u32, u32) { + match self { + Memory::Host(image) => image.dimensions(), + Memory::Device(entry) => entry.size(), + Memory::NotFound => (1, 1), + Memory::Invalid => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + map: HashMap<u64, Memory>, + hits: HashSet<u64>, +} + +impl Cache { + pub fn new() -> Self { + Self { + map: HashMap::new(), + hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { + if self.contains(handle) { + return self.get(handle).unwrap(); + } + + let memory = match handle.data() { + image::Data::Path(path) => { + if let Ok(image) = ::image::open(path) { + Memory::Host(image.to_bgra()) + } else { + Memory::NotFound + } + } + image::Data::Bytes(bytes) => { + if let Ok(image) = ::image::load_from_memory(&bytes) { + Memory::Host(image.to_bgra()) + } else { + Memory::Invalid + } + } + image::Data::Pixels { + width, + height, + pixels, + } => { + if let Some(image) = ::image::ImageBuffer::from_vec( + *width, + *height, + pixels.to_vec(), + ) { + Memory::Host(image) + } else { + Memory::Invalid + } + } + }; + + self.insert(handle, memory); + self.get(handle).unwrap() + } + + pub fn upload( + &mut self, + handle: &image::Handle, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let memory = self.load(handle); + + if let Memory::Host(image) = memory { + let (width, height) = image.dimensions(); + + let entry = atlas.upload(width, height, &image, device, encoder)?; + + *memory = Memory::Device(entry); + } + + if let Memory::Device(allocation) = memory { + Some(allocation) + } else { + None + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let hits = &self.hits; + + self.map.retain(|k, memory| { + let retain = hits.contains(k); + + if !retain { + if let Memory::Device(entry) = memory { + atlas.remove(entry); + } + } + + retain + }); + + self.hits.clear(); + } + + fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); + + self.map.get_mut(&handle.id()) + } + + fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + } + + fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } +} diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs new file mode 100644 index 00000000..bae0f82f --- /dev/null +++ b/wgpu/src/image/vector.rs @@ -0,0 +1,151 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::svg; +use std::collections::{HashMap, HashSet}; + +pub enum Svg { + Loaded(resvg::usvg::Tree), + NotFound, +} + +impl Svg { + pub fn viewport_dimensions(&self) -> (u32, u32) { + match self { + Svg::Loaded(tree) => { + let size = tree.svg_node().size; + + (size.width() as u32, size.height() as u32) + } + Svg::NotFound => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + svgs: HashMap<u64, Svg>, + rasterized: HashMap<(u64, u32, u32), atlas::Entry>, + svg_hits: HashSet<u64>, + rasterized_hits: HashSet<(u64, u32, u32)>, +} + +impl Cache { + pub fn new() -> Self { + Self { + svgs: HashMap::new(), + rasterized: HashMap::new(), + svg_hits: HashSet::new(), + rasterized_hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + let opt = resvg::Options::default(); + + let svg = match resvg::usvg::Tree::from_file(handle.path(), &opt.usvg) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + }; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + pub fn upload( + &mut self, + handle: &svg::Handle, + [width, height]: [f32; 2], + scale: f32, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture_atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * width).round() as u32, + (scale * height).round() as u32, + ); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&(id, width, height)) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + + return self.rasterized.get(&(id, width, height)); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let screen_size = + resvg::ScreenSize::new(width, height).unwrap(); + + let mut canvas = + resvg::raqote::DrawTarget::new(width as i32, height as i32); + + resvg::backend_raqote::render_to_canvas( + tree, + &resvg::Options::default(), + screen_size, + &mut canvas, + ); + + let allocation = texture_atlas.upload( + width, + height, + canvas.get_data(), + device, + encoder, + )?; + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized.insert((id, width, height), allocation); + + self.rasterized.get(&(id, width, height)) + } + Svg::NotFound => None, + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let svg_hits = &self.svg_hits; + let rasterized_hits = &self.rasterized_hits; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + atlas.remove(entry); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9f9ed8db..4e0cbc60 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -22,18 +22,36 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] -mod image; +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] +pub mod defaults; +pub mod settings; +pub mod triangle; +pub mod widget; +pub mod window; + mod primitive; mod quad; mod renderer; +mod target; mod text; mod transformation; +mod viewport; + +pub use wgpu; + +pub use defaults::Defaults; +pub use primitive::Primitive; +pub use renderer::Renderer; +pub use settings::Settings; +pub use target::Target; +pub use viewport::Viewport; + +#[doc(no_inline)] +pub use widget::*; -pub(crate) use crate::image::Image; pub(crate) use quad::Quad; pub(crate) use transformation::Transformation; -pub use primitive::Primitive; -pub use renderer::{Renderer, Target}; +#[cfg(any(feature = "image", feature = "svg"))] +mod image; diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 9efd74f6..823b4b72 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,8 +1,11 @@ use iced_native::{ - Background, Color, Font, HorizontalAlignment, Rectangle, Vector, - VerticalAlignment, + image, svg, Background, Color, Font, HorizontalAlignment, Point, Rectangle, + Vector, VerticalAlignment, }; +use crate::triangle; +use std::sync::Arc; + /// A rendering primitive. #[derive(Debug, Clone)] pub enum Primitive { @@ -38,14 +41,26 @@ pub enum Primitive { background: Background, /// The border radius of the quad border_radius: u16, + /// The border width of the quad + border_width: u16, + /// The border color of the quad + border_color: Color, }, /// An image primitive Image { - /// The path of the image - path: String, + /// The handle of the image + handle: image::Handle, /// The bounds of the image bounds: Rectangle, }, + /// An SVG primitive + Svg { + /// The path of the SVG file + handle: svg::Handle, + + /// The bounds of the viewport + bounds: Rectangle, + }, /// A clip primitive Clip { /// The bounds of the clip @@ -55,4 +70,20 @@ pub enum Primitive { /// The content of the clip content: Box<Primitive>, }, + /// A low-level primitive to render a mesh of triangles. + /// + /// It can be used to render many kinds of geometry freely. + Mesh2D { + /// The top-left coordinate of the mesh + origin: Point, + + /// The vertex and index buffers of the mesh + buffers: Arc<triangle::Mesh2D>, + }, +} + +impl Default for Primitive { + fn default() -> Primitive { + Primitive::None + } } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 3d797758..9047080d 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -14,7 +14,10 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &mut wgpu::Device) -> Pipeline { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + ) -> Pipeline { let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { bindings: &[wgpu::BindGroupLayoutBinding { @@ -79,7 +82,7 @@ impl Pipeline { }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format, color_blend: wgpu::BlendDescriptor { src_factor: wgpu::BlendFactor::SrcAlpha, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, @@ -125,9 +128,19 @@ impl Pipeline { }, wgpu::VertexAttributeDescriptor { shader_location: 4, - format: wgpu::VertexFormat::Float, + format: wgpu::VertexFormat::Float4, offset: 4 * (2 + 2 + 4), }, + wgpu::VertexAttributeDescriptor { + shader_location: 5, + format: wgpu::VertexFormat::Float, + offset: 4 * (2 + 2 + 4 + 4), + }, + wgpu::VertexAttributeDescriptor { + shader_location: 6, + format: wgpu::VertexFormat::Float, + offset: 4 * (2 + 2 + 4 + 4 + 1), + }, ], }, ], @@ -233,7 +246,8 @@ impl Pipeline { bounds.x, bounds.y, bounds.width, - bounds.height, + // TODO: Address anti-aliasing adjustments properly + bounds.height + 1, ); render_pass.draw_indexed( @@ -248,6 +262,7 @@ impl Pipeline { } } +#[repr(C)] #[derive(Clone, Copy)] pub struct Vertex { _position: [f32; 2], @@ -270,12 +285,15 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; +#[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Quad { pub position: [f32; 2], pub scale: [f32; 2], pub color: [f32; 4], + pub border_color: [f32; 4], pub border_radius: f32, + pub border_width: f32, } impl Quad { diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs index f27a4b8a..1da19b1a 100644 --- a/wgpu/src/renderer.rs +++ b/wgpu/src/renderer.rs @@ -1,37 +1,41 @@ -use crate::{quad, text, Image, Primitive, Quad, Transformation}; -use iced_native::{ - renderer::{Debugger, Windowed}, - Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, Widget, +use crate::{ + quad, text, triangle, Defaults, Primitive, Quad, Settings, Target, + Transformation, }; -use wgpu::{ - Adapter, BackendBit, CommandEncoderDescriptor, Device, DeviceDescriptor, - Extensions, Limits, PowerPreference, Queue, RequestAdapterOptions, +#[cfg(any(feature = "image", feature = "svg"))] +use crate::image::{self, Image}; + +use iced_native::{ + layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, + Widget, }; +use std::sync::Arc; -mod target; mod widget; -pub use target::Target; - /// A [`wgpu`] renderer. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs #[derive(Debug)] pub struct Renderer { - device: Device, - queue: Queue, quad_pipeline: quad::Pipeline, - image_pipeline: crate::image::Pipeline, text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: image::Pipeline, } struct Layer<'a> { bounds: Rectangle<u32>, offset: Vector<u32>, quads: Vec<Quad>, - images: Vec<Image>, + meshes: Vec<(Point, Arc<triangle::Mesh2D>)>, text: Vec<wgpu_glyph::Section<'a>>, + + #[cfg(any(feature = "image", feature = "svg"))] + images: Vec<Image>, } impl<'a> Layer<'a> { @@ -40,72 +44,62 @@ impl<'a> Layer<'a> { bounds, offset, quads: Vec::new(), - images: Vec::new(), text: Vec::new(), + meshes: Vec::new(), + + #[cfg(any(feature = "image", feature = "svg"))] + images: Vec::new(), } } } impl Renderer { - fn new() -> Self { - let adapter = Adapter::request(&RequestAdapterOptions { - power_preference: PowerPreference::LowPower, - backends: BackendBit::all(), - }) - .expect("Request adapter"); - - let (mut device, queue) = adapter.request_device(&DeviceDescriptor { - extensions: Extensions { - anisotropic_filtering: false, - }, - limits: Limits { max_bind_groups: 2 }, - }); + /// Creates a new [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + pub fn new(device: &mut wgpu::Device, settings: Settings) -> Self { + let text_pipeline = + text::Pipeline::new(device, settings.format, settings.default_font); + let quad_pipeline = quad::Pipeline::new(device, settings.format); + let triangle_pipeline = triangle::Pipeline::new( + device, + settings.format, + settings.antialiasing, + ); - let text_pipeline = text::Pipeline::new(&mut device); - let quad_pipeline = quad::Pipeline::new(&mut device); - let image_pipeline = crate::image::Pipeline::new(&mut device); + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = image::Pipeline::new(device, settings.format); Self { - device, - queue, quad_pipeline, - image_pipeline, text_pipeline, + triangle_pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, } } - fn draw<T: AsRef<str>>( + /// Draws the provided primitives in the given [`Target`]. + /// + /// The text provided as overlay will be renderer on top of the primitives. + /// This is useful for rendering debug information. + /// + /// [`Target`]: struct.Target.html + pub fn draw<T: AsRef<str>>( &mut self, + device: &mut wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: Target<'_>, (primitive, mouse_cursor): &(Primitive, MouseCursor), + scale_factor: f64, overlay: &[T], - target: &mut Target, ) -> MouseCursor { log::debug!("Drawing"); - let (width, height) = target.dimensions(); - let dpi = target.dpi(); - let transformation = target.transformation(); - let frame = target.next_frame(); - - let mut encoder = self - .device - .create_command_encoder(&CommandEncoderDescriptor { todo: 0 }); - - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &frame.view, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }, - }], - depth_stencil_attachment: None, - }); + let (width, height) = target.viewport.dimensions(); + let scale_factor = scale_factor as f32; + let transformation = target.viewport.transformation(); let mut layers = Vec::new(); @@ -123,10 +117,20 @@ impl Renderer { self.draw_overlay(overlay, &mut layers); for layer in layers { - self.flush(dpi, transformation, &layer, &mut encoder, &frame.view); + self.flush( + device, + scale_factor, + transformation, + &layer, + encoder, + target.texture, + width, + height, + ); } - self.queue.submit(&[encoder.finish()]); + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.trim_cache(); *mouse_cursor } @@ -215,6 +219,8 @@ impl Renderer { bounds, background, border_radius, + border_width, + border_color, } => { // TODO: Move some of this computations to the GPU (?) layer.quads.push(Quad { @@ -227,39 +233,30 @@ impl Renderer { Background::Color(color) => color.into_linear(), }, border_radius: *border_radius as f32, + border_width: *border_width as f32, + border_color: border_color.into_linear(), }); } - Primitive::Image { path, bounds } => { - layer.images.push(Image { - path: path.clone(), - position: [bounds.x, bounds.y], - scale: [bounds.width, bounds.height], - }); + Primitive::Mesh2D { origin, buffers } => { + layer.meshes.push((*origin, buffers.clone())); } Primitive::Clip { bounds, offset, content, } => { - let x = bounds.x - layer.offset.x as f32; - let y = bounds.y - layer.offset.y as f32; - let width = (bounds.width + x).min(bounds.width); - let height = (bounds.height + y).min(bounds.height); - - // Only draw visible content on-screen - // TODO: Also, check for parent layer bounds to avoid further - // drawing in some circumstances. - if width > 0.0 && height > 0.0 { - let clip_layer = Layer::new( - Rectangle { - x: x.max(0.0).floor() as u32, - y: y.max(0.0).floor() as u32, - width: width.ceil() as u32, - height: height.ceil() as u32, - }, - layer.offset + *offset, - ); + let layer_bounds: Rectangle<f32> = layer.bounds.into(); + let clip = Rectangle { + x: bounds.x - layer.offset.x as f32, + y: bounds.y - layer.offset.y as f32, + ..*bounds + }; + + // Only draw visible content + if let Some(clip_bounds) = layer_bounds.intersection(&clip) { + let clip_layer = + Layer::new(clip_bounds.into(), layer.offset + *offset); let new_layer = Layer::new(layer.bounds, layer.offset); layers.push(clip_layer); @@ -267,6 +264,28 @@ impl Renderer { layers.push(new_layer); } } + + #[cfg(feature = "image")] + Primitive::Image { handle, bounds } => { + layer.images.push(Image { + handle: image::Handle::Raster(handle.clone()), + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "image"))] + Primitive::Image { .. } => {} + + #[cfg(feature = "svg")] + Primitive::Svg { handle, bounds } => { + layer.images.push(Image { + handle: image::Handle::Vector(handle.clone()), + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "svg"))] + Primitive::Svg { .. } => {} } } @@ -306,42 +325,69 @@ impl Renderer { fn flush( &mut self, - dpi: f32, + device: &mut wgpu::Device, + scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, + target_width: u32, + target_height: u32, ) { - let bounds = layer.bounds * dpi; + let bounds = layer.bounds * scale_factor; + + if layer.meshes.len() > 0 { + let translated = transformation + * Transformation::scale(scale_factor, scale_factor) + * Transformation::translate( + -(layer.offset.x as f32), + -(layer.offset.y as f32), + ); + + self.triangle_pipeline.draw( + device, + encoder, + target, + target_width, + target_height, + translated, + &layer.meshes, + bounds, + ); + } if layer.quads.len() > 0 { self.quad_pipeline.draw( - &mut self.device, + device, encoder, &layer.quads, transformation, - dpi, + scale_factor, bounds, target, ); } - if layer.images.len() > 0 { - let translated_and_scaled = transformation - * Transformation::scale(dpi, dpi) - * Transformation::translate( - -(layer.offset.x as f32), - -(layer.offset.y as f32), - ); + #[cfg(any(feature = "image", feature = "svg"))] + { + if layer.images.len() > 0 { + let translated_and_scaled = transformation + * Transformation::scale(scale_factor, scale_factor) + * Transformation::translate( + -(layer.offset.x as f32), + -(layer.offset.y as f32), + ); - self.image_pipeline.draw( - &mut self.device, - encoder, - &layer.images, - translated_and_scaled, - bounds, - target, - ); + self.image_pipeline.draw( + device, + encoder, + &layer.images, + translated_and_scaled, + bounds, + target, + scale_factor, + ); + } } if layer.text.len() > 0 { @@ -353,25 +399,25 @@ impl Renderer { // bit "jumpy". We may be able to do better once we improve // our text rendering/caching pipeline. screen_position: ( - (text.screen_position.0 * dpi).round(), - (text.screen_position.1 * dpi).round(), + (text.screen_position.0 * scale_factor).round(), + (text.screen_position.1 * scale_factor).round(), ), - // TODO: Fix precision issues with some DPI factors. + // TODO: Fix precision issues with some scale factors. // // The `ceil` here can cause some words to render on the // same line when they should not. // // Ideally, `wgpu_glyph` should be able to compute layout // using logical positions, and then apply the proper - // DPI scaling. This would ensure that both measuring and - // rendering follow the same layout rules. + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. bounds: ( - (text.bounds.0 * dpi).ceil(), - (text.bounds.1 * dpi).ceil(), + (text.bounds.0 * scale_factor).ceil(), + (text.bounds.1 * scale_factor).ceil(), ), scale: wgpu_glyph::Scale { - x: text.scale.x * dpi, - y: text.scale.y * dpi, + x: text.scale.x * scale_factor, + y: text.scale.y * scale_factor, }, ..*text }; @@ -380,7 +426,7 @@ impl Renderer { } self.text_pipeline.draw_queued( - &mut self.device, + device, encoder, target, transformation, @@ -397,12 +443,14 @@ impl Renderer { impl iced_native::Renderer for Renderer { type Output = (Primitive, MouseCursor); + type Defaults = Defaults; fn layout<'a, Message>( &mut self, element: &iced_native::Element<'a, Message, Self>, + limits: &iced_native::layout::Limits, ) -> iced_native::layout::Node { - let node = element.layout(self, &iced_native::layout::Limits::NONE); + let node = element.layout(self, limits); self.text_pipeline.clear_measurement_cache(); @@ -410,33 +458,18 @@ impl iced_native::Renderer for Renderer { } } -impl Windowed for Renderer { - type Target = Target; - - fn new() -> Self { - Self::new() - } - - fn draw<T: AsRef<str>>( - &mut self, - output: &Self::Output, - overlay: &[T], - target: &mut Target, - ) -> MouseCursor { - self.draw(output, overlay, target) - } -} - -impl Debugger for Renderer { +impl layout::Debugger for Renderer { fn explain<Message>( &mut self, + defaults: &Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, color: Color, ) -> Self::Output { let mut primitives = Vec::new(); - let (primitive, cursor) = widget.draw(self, layout, cursor_position); + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position); explain_layout(layout, color, &mut primitives); primitives.push(primitive); @@ -450,11 +483,12 @@ fn explain_layout( color: Color, primitives: &mut Vec<Primitive>, ) { - // TODO: Draw borders instead primitives.push(Primitive::Quad { bounds: layout.bounds(), - background: Background::Color([0.0, 0.0, 0.0, 0.05].into()), + background: Background::Color(Color::TRANSPARENT), border_radius: 0, + border_width: 1, + border_color: [0.6, 0.6, 0.6, 0.5].into(), }); for child in layout.children() { diff --git a/wgpu/src/renderer/target.rs b/wgpu/src/renderer/target.rs deleted file mode 100644 index 569f3c62..00000000 --- a/wgpu/src/renderer/target.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{Renderer, Transformation}; - -use raw_window_handle::HasRawWindowHandle; - -/// A rendering target. -#[derive(Debug)] -pub struct Target { - surface: wgpu::Surface, - width: u16, - height: u16, - dpi: f32, - transformation: Transformation, - swap_chain: wgpu::SwapChain, -} - -impl Target { - pub(crate) fn dimensions(&self) -> (u16, u16) { - (self.width, self.height) - } - - pub(crate) fn dpi(&self) -> f32 { - self.dpi - } - - pub(crate) fn transformation(&self) -> Transformation { - self.transformation - } - - pub(crate) fn next_frame(&mut self) -> wgpu::SwapChainOutput<'_> { - self.swap_chain.get_next_texture() - } -} - -impl iced_native::renderer::Target for Target { - type Renderer = Renderer; - - fn new<W: HasRawWindowHandle>( - window: &W, - width: u16, - height: u16, - dpi: f32, - renderer: &Renderer, - ) -> Target { - let surface = wgpu::Surface::create(window); - let swap_chain = - new_swap_chain(&surface, width, height, &renderer.device); - - Target { - surface, - width, - height, - dpi, - transformation: Transformation::orthographic(width, height), - swap_chain, - } - } - - fn resize( - &mut self, - width: u16, - height: u16, - dpi: f32, - renderer: &Renderer, - ) { - self.width = width; - self.height = height; - self.dpi = dpi; - self.transformation = Transformation::orthographic(width, height); - self.swap_chain = - new_swap_chain(&self.surface, width, height, &renderer.device); - } -} - -fn new_swap_chain( - surface: &wgpu::Surface, - width: u16, - height: u16, - device: &wgpu::Device, -) -> wgpu::SwapChain { - device.create_swap_chain( - &surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: u32::from(width), - height: u32::from(height), - present_mode: wgpu::PresentMode::Vsync, - }, - ) -} diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs index 52410bee..84f908e7 100644 --- a/wgpu/src/renderer/widget.rs +++ b/wgpu/src/renderer/widget.rs @@ -1,10 +1,18 @@ mod button; mod checkbox; mod column; -mod image; +mod container; +mod progress_bar; mod radio; mod row; mod scrollable; mod slider; +mod space; mod text; mod text_input; + +#[cfg(feature = "svg")] +mod svg; + +#[cfg(feature = "image")] +mod image; diff --git a/wgpu/src/renderer/widget/button.rs b/wgpu/src/renderer/widget/button.rs index 86963053..0de5bf5c 100644 --- a/wgpu/src/renderer/widget/button.rs +++ b/wgpu/src/renderer/widget/button.rs @@ -1,54 +1,88 @@ -use crate::{Primitive, Renderer}; -use iced_native::{button, Background, MouseCursor, Point, Rectangle}; +use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; +use iced_native::{ + Background, Color, Element, Layout, MouseCursor, Point, Rectangle, Vector, +}; -impl button::Renderer for Renderer { - fn draw( +impl iced_native::button::Renderer for Renderer { + const DEFAULT_PADDING: u16 = 5; + + type Style = Box<dyn StyleSheet>; + + fn draw<Message>( &mut self, + defaults: &Defaults, bounds: Rectangle, cursor_position: Point, + is_disabled: bool, is_pressed: bool, - background: Option<Background>, - border_radius: u16, - (content, _): Self::Output, + style: &Box<dyn StyleSheet>, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); - // TODO: Render proper shadows - // TODO: Make hovering and pressed styles configurable - let shadow_offset = if is_mouse_over { + let styling = if is_disabled { + style.disabled() + } else if is_mouse_over { if is_pressed { - 0.0 + style.pressed() } else { - 2.0 + style.hovered() } } else { - 1.0 + style.active() }; + let (content, _) = content.draw( + self, + &Defaults { + text: defaults::Text { + color: styling.text_color, + }, + ..*defaults + }, + content_layout, + cursor_position, + ); + ( - match background { - None => content, - Some(background) => Primitive::Group { - primitives: vec![ - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + shadow_offset, - ..bounds - }, - background: Background::Color( - [0.0, 0.0, 0.0, 0.5].into(), - ), - border_radius, - }, - Primitive::Quad { - bounds, - background, - border_radius, + if styling.background.is_some() || styling.border_width > 0 { + let background = Primitive::Quad { + bounds, + background: styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }; + + if styling.shadow_offset == Vector::default() { + Primitive::Group { + primitives: vec![background, content], + } + } else { + // TODO: Implement proper shadow support + let shadow = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds }, - content, - ], - }, + background: Background::Color( + [0.0, 0.0, 0.0, 0.5].into(), + ), + border_radius: styling.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![shadow, background, content], + } + } + } else { + content }, if is_mouse_over { MouseCursor::Pointer diff --git a/wgpu/src/renderer/widget/checkbox.rs b/wgpu/src/renderer/widget/checkbox.rs index 54b4b1cc..1a0585d3 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/wgpu/src/renderer/widget/checkbox.rs @@ -1,15 +1,13 @@ -use crate::{Primitive, Renderer}; +use crate::{checkbox::StyleSheet, Primitive, Renderer}; use iced_native::{ - checkbox, Background, HorizontalAlignment, MouseCursor, Rectangle, - VerticalAlignment, + checkbox, HorizontalAlignment, MouseCursor, Rectangle, VerticalAlignment, }; -const SIZE: f32 = 28.0; - impl checkbox::Renderer for Renderer { - fn default_size(&self) -> u32 { - SIZE as u32 - } + type Style = Box<dyn StyleSheet>; + + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, @@ -17,31 +15,21 @@ impl checkbox::Renderer for Renderer { is_checked: bool, is_mouse_over: bool, (label, _): Self::Output, + style_sheet: &Self::Style, ) -> Self::Output { - let (checkbox_border, checkbox_box) = ( - Primitive::Quad { - bounds, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: 6, - }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color( - if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: 5, - }, - ); + let style = if is_mouse_over { + style_sheet.hovered(is_checked) + } else { + style_sheet.active(is_checked) + }; + + let checkbox = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; ( Primitive::Group { @@ -51,14 +39,14 @@ impl checkbox::Renderer for Renderer { font: crate::text::BUILTIN_ICONS, size: bounds.height * 0.7, bounds: bounds, - color: [0.3, 0.3, 0.3].into(), + color: style.checkmark_color, horizontal_alignment: HorizontalAlignment::Center, vertical_alignment: VerticalAlignment::Center, }; - vec![checkbox_border, checkbox_box, check, label] + vec![checkbox, check, label] } else { - vec![checkbox_border, checkbox_box, label] + vec![checkbox, label] }, }, if is_mouse_over { diff --git a/wgpu/src/renderer/widget/column.rs b/wgpu/src/renderer/widget/column.rs index 6c31af90..95a7463a 100644 --- a/wgpu/src/renderer/widget/column.rs +++ b/wgpu/src/renderer/widget/column.rs @@ -4,6 +4,7 @@ use iced_native::{column, Element, Layout, MouseCursor, Point}; impl column::Renderer for Renderer { fn draw<Message>( &mut self, + defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, @@ -17,7 +18,7 @@ impl column::Renderer for Renderer { .zip(layout.children()) .map(|(child, layout)| { let (primitive, new_mouse_cursor) = - child.draw(self, layout, cursor_position); + child.draw(self, defaults, layout, cursor_position); if new_mouse_cursor > mouse_cursor { mouse_cursor = new_mouse_cursor; diff --git a/wgpu/src/renderer/widget/container.rs b/wgpu/src/renderer/widget/container.rs new file mode 100644 index 00000000..2d4d1db8 --- /dev/null +++ b/wgpu/src/renderer/widget/container.rs @@ -0,0 +1,49 @@ +use crate::{container, defaults, Defaults, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; + +impl iced_native::container::Renderer for Renderer { + type Style = Box<dyn container::StyleSheet>; + + fn draw<Message>( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + style_sheet: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + ..*defaults + }; + + let (content, mouse_cursor) = + content.draw(self, &defaults, content_layout, cursor_position); + + if style.background.is_some() || style.border_width > 0 { + let quad = Primitive::Quad { + bounds, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: vec![quad, content], + }, + mouse_cursor, + ) + } else { + (content, mouse_cursor) + } + } +} diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs index 0006dde1..70dc5d97 100644 --- a/wgpu/src/renderer/widget/image.rs +++ b/wgpu/src/renderer/widget/image.rs @@ -2,14 +2,18 @@ use crate::{Primitive, Renderer}; use iced_native::{image, Layout, MouseCursor}; impl image::Renderer for Renderer { - fn dimensions(&self, path: &str) -> (u32, u32) { - self.image_pipeline.dimensions(path) + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.image_pipeline.dimensions(handle) } - fn draw(&mut self, path: &str, layout: Layout<'_>) -> Self::Output { + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { ( Primitive::Image { - path: String::from(path), + handle, bounds: layout.bounds(), }, MouseCursor::OutOfBounds, diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/wgpu/src/renderer/widget/progress_bar.rs new file mode 100644 index 00000000..34e33276 --- /dev/null +++ b/wgpu/src/renderer/widget/progress_bar.rs @@ -0,0 +1,54 @@ +use crate::{progress_bar::StyleSheet, Primitive, Renderer}; +use iced_native::{progress_bar, Color, MouseCursor, Rectangle}; + +impl progress_bar::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + bounds: Rectangle, + range: std::ops::RangeInclusive<f32>, + value: f32, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = style_sheet.style(); + + let (range_start, range_end) = range.into_inner(); + let active_progress_width = bounds.width + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let background = Primitive::Group { + primitives: vec![Primitive::Quad { + bounds: Rectangle { ..bounds }, + background: style.background, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }], + }; + + ( + if active_progress_width > 0.0 { + let bar = Primitive::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + background: style.bar, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![background, bar], + } + } else { + background + }, + MouseCursor::OutOfBounds, + ) + } +} diff --git a/wgpu/src/renderer/widget/radio.rs b/wgpu/src/renderer/widget/radio.rs index 3c00a4c2..564f066b 100644 --- a/wgpu/src/renderer/widget/radio.rs +++ b/wgpu/src/renderer/widget/radio.rs @@ -1,10 +1,12 @@ -use crate::{Primitive, Renderer}; -use iced_native::{radio, Background, MouseCursor, Rectangle}; +use crate::{radio::StyleSheet, Primitive, Renderer}; +use iced_native::{radio, Background, Color, MouseCursor, Rectangle}; const SIZE: f32 = 28.0; const DOT_SIZE: f32 = SIZE / 2.0; impl radio::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn default_size(&self) -> u32 { SIZE as u32 } @@ -15,31 +17,21 @@ impl radio::Renderer for Renderer { is_selected: bool, is_mouse_over: bool, (label, _): Self::Output, + style_sheet: &Self::Style, ) -> Self::Output { - let (radio_border, radio_box) = ( - Primitive::Quad { - bounds, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: (SIZE / 2.0) as u16, - }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color( - if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: (SIZE / 2.0 - 1.0) as u16, - }, - ); + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let radio = Primitive::Quad { + bounds, + background: style.background, + border_radius: (SIZE / 2.0) as u16, + border_width: style.border_width, + border_color: style.border_color, + }; ( Primitive::Group { @@ -51,13 +43,15 @@ impl radio::Renderer for Renderer { width: bounds.width - DOT_SIZE, height: bounds.height - DOT_SIZE, }, - background: Background::Color([0.3, 0.3, 0.3].into()), + background: Background::Color(style.dot_color), border_radius: (DOT_SIZE / 2.0) as u16, + border_width: 0, + border_color: Color::TRANSPARENT, }; - vec![radio_border, radio_box, radio_circle, label] + vec![radio, radio_circle, label] } else { - vec![radio_border, radio_box, label] + vec![radio, label] }, }, if is_mouse_over { diff --git a/wgpu/src/renderer/widget/row.rs b/wgpu/src/renderer/widget/row.rs index f082dc61..bd9f1a04 100644 --- a/wgpu/src/renderer/widget/row.rs +++ b/wgpu/src/renderer/widget/row.rs @@ -4,6 +4,7 @@ use iced_native::{row, Element, Layout, MouseCursor, Point}; impl row::Renderer for Renderer { fn draw<Message>( &mut self, + defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, @@ -17,7 +18,7 @@ impl row::Renderer for Renderer { .zip(layout.children()) .map(|(child, layout)| { let (primitive, new_mouse_cursor) = - child.draw(self, layout, cursor_position); + child.draw(self, defaults, layout, cursor_position); if new_mouse_cursor > mouse_cursor { mouse_cursor = new_mouse_cursor; diff --git a/wgpu/src/renderer/widget/scrollable.rs b/wgpu/src/renderer/widget/scrollable.rs index 58dc3df9..bfee7411 100644 --- a/wgpu/src/renderer/widget/scrollable.rs +++ b/wgpu/src/renderer/widget/scrollable.rs @@ -1,45 +1,63 @@ use crate::{Primitive, Renderer}; use iced_native::{ - scrollable, Background, MouseCursor, Point, Rectangle, Vector, + scrollable, Background, Color, MouseCursor, Rectangle, Vector, }; const SCROLLBAR_WIDTH: u16 = 10; const SCROLLBAR_MARGIN: u16 = 2; -fn scrollbar_bounds(bounds: Rectangle) -> Rectangle { - Rectangle { - x: bounds.x + bounds.width - - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - y: bounds.y, - width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - height: bounds.height, - } -} - impl scrollable::Renderer for Renderer { - fn is_mouse_over_scrollbar( + type Style = Box<dyn iced_style::scrollable::StyleSheet>; + + fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, - cursor_position: Point, - ) -> bool { - content_bounds.height > bounds.height - && scrollbar_bounds(bounds).contains(cursor_position) + offset: u32, + ) -> Option<scrollable::Scrollbar> { + if content_bounds.height > bounds.height { + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + y: bounds.y, + width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scrollbar_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + y: scrollbar_bounds.y + y_offset, + width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), + height: scrollbar_height, + }; + + Some(scrollable::Scrollbar { + bounds: scrollbar_bounds, + scroller: scrollable::Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } } fn draw( &mut self, state: &scrollable::State, bounds: Rectangle, - content_bounds: Rectangle, + _content_bounds: Rectangle, is_mouse_over: bool, is_mouse_over_scrollbar: bool, + scrollbar: Option<scrollable::Scrollbar>, offset: u32, + style_sheet: &Self::Style, (content, mouse_cursor): Self::Output, ) -> Self::Output { - let is_content_overflowing = content_bounds.height > bounds.height; - let scrollbar_bounds = scrollbar_bounds(bounds); - let clip = Primitive::Clip { bounds, offset: Vector::new(0, offset), @@ -47,51 +65,59 @@ impl scrollable::Renderer for Renderer { }; ( - if is_content_overflowing - && (is_mouse_over || state.is_scrollbar_grabbed()) - { - let ratio = bounds.height / content_bounds.height; - let scrollbar_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; + if let Some(scrollbar) = scrollbar { + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0; - let scrollbar = Primitive::Quad { - bounds: Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), - y: scrollbar_bounds.y + y_offset, - width: scrollbar_bounds.width - - f32::from(2 * SCROLLBAR_MARGIN), - height: scrollbar_height, - }, - background: Background::Color([0.0, 0.0, 0.0, 0.7].into()), - border_radius: 5, + let scroller = if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + Primitive::Quad { + bounds: scrollbar.scroller.bounds, + background: Background::Color(style.scroller.color), + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + } + } else { + Primitive::None }; - if is_mouse_over_scrollbar || state.is_scrollbar_grabbed() { - let scrollbar_background = Primitive::Quad { + let scrollbar = if is_scrollbar_visible { + Primitive::Quad { bounds: Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), - width: scrollbar_bounds.width + x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), + width: scrollbar.bounds.width - f32::from(2 * SCROLLBAR_MARGIN), - ..scrollbar_bounds + ..scrollbar.bounds }, - background: Background::Color( - [0.0, 0.0, 0.0, 0.3].into(), - ), - border_radius: 5, - }; - - Primitive::Group { - primitives: vec![clip, scrollbar_background, scrollbar], + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, } } else { - Primitive::Group { - primitives: vec![clip, scrollbar], - } + Primitive::None + }; + + Primitive::Group { + primitives: vec![clip, scrollbar, scroller], } } else { clip }, - if is_mouse_over_scrollbar || state.is_scrollbar_grabbed() { + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { MouseCursor::Idle } else { mouse_cursor diff --git a/wgpu/src/renderer/widget/slider.rs b/wgpu/src/renderer/widget/slider.rs index f561be0a..c8ebd0da 100644 --- a/wgpu/src/renderer/widget/slider.rs +++ b/wgpu/src/renderer/widget/slider.rs @@ -1,10 +1,14 @@ -use crate::{Primitive, Renderer}; +use crate::{ + slider::{HandleShape, StyleSheet}, + Primitive, Renderer, +}; use iced_native::{slider, Background, Color, MouseCursor, Point, Rectangle}; -const HANDLE_WIDTH: f32 = 8.0; const HANDLE_HEIGHT: f32 = 22.0; impl slider::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn height(&self) -> u32 { 30 } @@ -16,9 +20,18 @@ impl slider::Renderer for Renderer { range: std::ops::RangeInclusive<f32>, value: f32, is_dragging: bool, + style_sheet: &Self::Style, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); + let style = if is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + let rail_y = bounds.y + (bounds.height / 2.0).round(); let (rail_top, rail_bottom) = ( @@ -29,8 +42,10 @@ impl slider::Renderer for Renderer { width: bounds.width, height: 2.0, }, - background: Background::Color([0.6, 0.6, 0.6].into()), + background: Background::Color(style.rail_colors.0), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }, Primitive::Quad { bounds: Rectangle { @@ -39,51 +54,45 @@ impl slider::Renderer for Renderer { width: bounds.width, height: 2.0, }, - background: Background::Color(Color::WHITE), + background: Background::Color(style.rail_colors.1), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }, ); let (range_start, range_end) = range.into_inner(); - let handle_offset = (bounds.width - HANDLE_WIDTH) + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (f32::from(radius * 2), f32::from(radius * 2), radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), HANDLE_HEIGHT, border_radius), + }; + + let handle_offset = (bounds.width - handle_width) * ((value - range_start) / (range_end - range_start).max(1.0)); - let (handle_border, handle) = ( - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round() - 1.0, - y: rail_y - HANDLE_HEIGHT / 2.0 - 1.0, - width: HANDLE_WIDTH + 2.0, - height: HANDLE_HEIGHT + 2.0, - }, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: 5, + let handle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - HANDLE_HEIGHT / 2.0, - width: HANDLE_WIDTH, - height: HANDLE_HEIGHT, - }, - background: Background::Color( - if is_dragging { - [0.85, 0.85, 0.85] - } else if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: 4, - }, - ); + background: Background::Color(style.handle.color), + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }; ( Primitive::Group { - primitives: vec![rail_top, rail_bottom, handle_border, handle], + primitives: vec![rail_top, rail_bottom, handle], }, if is_dragging { MouseCursor::Grabbing diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs new file mode 100644 index 00000000..28e05437 --- /dev/null +++ b/wgpu/src/renderer/widget/space.rs @@ -0,0 +1,8 @@ +use crate::{Primitive, Renderer}; +use iced_native::{space, MouseCursor, Rectangle}; + +impl space::Renderer for Renderer { + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, MouseCursor::OutOfBounds) + } +} diff --git a/wgpu/src/renderer/widget/svg.rs b/wgpu/src/renderer/widget/svg.rs new file mode 100644 index 00000000..67bc3fe1 --- /dev/null +++ b/wgpu/src/renderer/widget/svg.rs @@ -0,0 +1,22 @@ +use crate::{Primitive, Renderer}; +use iced_native::{svg, Layout, MouseCursor}; + +impl svg::Renderer for Renderer { + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.image_pipeline.viewport_dimensions(handle) + } + + fn draw( + &mut self, + handle: svg::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Svg { + handle, + bounds: layout.bounds(), + }, + MouseCursor::OutOfBounds, + ) + } +} diff --git a/wgpu/src/renderer/widget/text.rs b/wgpu/src/renderer/widget/text.rs index 08a162ba..33e549cd 100644 --- a/wgpu/src/renderer/widget/text.rs +++ b/wgpu/src/renderer/widget/text.rs @@ -6,13 +6,8 @@ use iced_native::{ use std::f32; -// TODO: Obtain from renderer configuration -const DEFAULT_TEXT_SIZE: f32 = 20.0; - impl text::Renderer for Renderer { - fn default_size(&self) -> u16 { - DEFAULT_TEXT_SIZE as u16 - } + const DEFAULT_SIZE: u16 = 20; fn measure( &self, @@ -27,6 +22,7 @@ impl text::Renderer for Renderer { fn draw( &mut self, + defaults: &Self::Defaults, bounds: Rectangle, content: &str, size: u16, @@ -40,7 +36,7 @@ impl text::Renderer for Renderer { content: content.to_string(), size: f32::from(size), bounds, - color: color.unwrap_or(Color::BLACK), + color: color.unwrap_or(defaults.text.color), font, horizontal_alignment, vertical_alignment, diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 9ed3b415..e2a1b3a9 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -1,4 +1,4 @@ -use crate::{Primitive, Renderer}; +use crate::{text_input::StyleSheet, Primitive, Renderer}; use iced_native::{ text_input, Background, Color, Font, HorizontalAlignment, MouseCursor, @@ -7,48 +7,85 @@ use iced_native::{ use std::f32; impl text_input::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn default_size(&self) -> u16 { // TODO: Make this configurable 20 } + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let (mut width, _) = self.text_pipeline.measure( + value, + f32::from(size), + font, + Size::INFINITY, + ); + + let spaces_at_the_end = value.len() - value.trim_end().len(); + + if spaces_at_the_end > 0 { + let space_width = self.text_pipeline.space_width(size as f32); + width += spaces_at_the_end as f32 * space_width; + } + + width + } + + fn offset( + &self, + text_bounds: Rectangle, + size: u16, + value: &text_input::Value, + state: &text_input::State, + font: Font, + ) -> f32 { + if state.is_focused() { + let (_, offset) = measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + state.cursor_position(value), + font, + ); + + offset + } else { + 0.0 + } + } + fn draw( &mut self, bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, size: u16, + font: Font, placeholder: &str, value: &text_input::Value, state: &text_input::State, + style_sheet: &Self::Style, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); - let border = Primitive::Quad { - bounds, - background: Background::Color( - if is_mouse_over || state.is_focused() { - [0.5, 0.5, 0.5] - } else { - [0.7, 0.7, 0.7] - } - .into(), - ), - border_radius: 5, + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() }; let input = Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color(Color::WHITE), - border_radius: 5, + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, }; - let size = f32::from(size); let text = value.to_string(); let text_value = Primitive::Text { @@ -58,40 +95,31 @@ impl text_input::Renderer for Renderer { text.clone() }, color: if text.is_empty() { - [0.7, 0.7, 0.7] + style_sheet.placeholder_color() } else { - [0.3, 0.3, 0.3] + style_sheet.value_color() } .into(), - font: Font::Default, + font, bounds: Rectangle { width: f32::INFINITY, ..text_bounds }, - size, + size: f32::from(size), horizontal_alignment: HorizontalAlignment::Left, vertical_alignment: VerticalAlignment::Center, }; let (contents_primitive, offset) = if state.is_focused() { - let text_before_cursor = - value.until(state.cursor_position(value)).to_string(); - - let (mut text_value_width, _) = self.text_pipeline.measure( - &text_before_cursor, + let (text_value_width, offset) = measure_cursor_and_scroll_offset( + self, + text_bounds, + value, size, - Font::Default, - Size::new(f32::INFINITY, text_bounds.height), + state.cursor_position(value), + font, ); - let spaces_at_the_end = - text_before_cursor.len() - text_before_cursor.trim_end().len(); - - if spaces_at_the_end > 0 { - let space_width = self.text_pipeline.space_width(size); - text_value_width += spaces_at_the_end as f32 * space_width; - } - let cursor = Primitive::Quad { bounds: Rectangle { x: text_bounds.x + text_value_width, @@ -99,19 +127,17 @@ impl text_input::Renderer for Renderer { width: 1.0, height: text_bounds.height, }, - background: Background::Color(Color::BLACK), + background: Background::Color(style_sheet.value_color()), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }; ( Primitive::Group { primitives: vec![text_value, cursor], }, - Vector::new( - ((text_value_width + 5.0) - text_bounds.width).max(0.0) - as u32, - 0, - ), + Vector::new(offset as u32, 0), ) } else { (text_value, Vector::new(0, 0)) @@ -125,7 +151,7 @@ impl text_input::Renderer for Renderer { ( Primitive::Group { - primitives: vec![border, input, contents], + primitives: vec![input, contents], }, if is_mouse_over { MouseCursor::Text @@ -135,3 +161,22 @@ impl text_input::Renderer for Renderer { ) } } + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &text_input::Value, + size: u16, + cursor_index: usize, + font: Font, +) -> (f32, f32) { + use iced_native::text_input::Renderer; + + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_value(&text_before_cursor, size, font); + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs new file mode 100644 index 00000000..f946ce0d --- /dev/null +++ b/wgpu/src/settings.rs @@ -0,0 +1,56 @@ +//! Configure a [`Renderer`]. +//! +//! [`Renderer`]: struct.Renderer.html + +/// The settings of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The output format of the [`Renderer`]. + /// + /// [`Renderer`]: ../struct.Renderer.html + pub format: wgpu::TextureFormat, + + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The antialiasing strategy that will be used for triangle primitives. + pub antialiasing: Option<Antialiasing>, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + default_font: None, + antialiasing: None, + } + } +} + +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + pub(crate) fn sample_count(&self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/wgpu/src/shader/blit.frag b/wgpu/src/shader/blit.frag new file mode 100644 index 00000000..dfed960f --- /dev/null +++ b/wgpu/src/shader/blit.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(location = 0) in vec2 v_Uv; + +layout(set = 0, binding = 0) uniform sampler u_Sampler; +layout(set = 1, binding = 0) uniform texture2D u_Texture; + +layout(location = 0) out vec4 o_Color; + +void main() { + o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv); +} diff --git a/wgpu/src/shader/blit.frag.spv b/wgpu/src/shader/blit.frag.spv Binary files differnew file mode 100644 index 00000000..2c5638b5 --- /dev/null +++ b/wgpu/src/shader/blit.frag.spv diff --git a/wgpu/src/shader/blit.vert b/wgpu/src/shader/blit.vert new file mode 100644 index 00000000..1c081b9e --- /dev/null +++ b/wgpu/src/shader/blit.vert @@ -0,0 +1,26 @@ +#version 450 + +layout(location = 0) out vec2 o_Uv; + +const vec2 positions[6] = vec2[6]( + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2(1.0, 1.0), + vec2(-1.0, -1.0), + vec2(1.0, -1.0), + vec2(1.0, 1.0) +); + +const vec2 uvs[6] = vec2[6]( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + o_Uv = uvs[gl_VertexIndex]; + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} diff --git a/wgpu/src/shader/blit.vert.spv b/wgpu/src/shader/blit.vert.spv Binary files differnew file mode 100644 index 00000000..ad697d48 --- /dev/null +++ b/wgpu/src/shader/blit.vert.spv diff --git a/wgpu/src/shader/image.frag b/wgpu/src/shader/image.frag index e35e455a..2809e9e6 100644 --- a/wgpu/src/shader/image.frag +++ b/wgpu/src/shader/image.frag @@ -1,12 +1,12 @@ #version 450 -layout(location = 0) in vec2 v_Uv; +layout(location = 0) in vec3 v_Uv; layout(set = 0, binding = 1) uniform sampler u_Sampler; -layout(set = 1, binding = 0) uniform texture2D u_Texture; +layout(set = 1, binding = 0) uniform texture2DArray u_Texture; layout(location = 0) out vec4 o_Color; void main() { - o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv); + o_Color = texture(sampler2DArray(u_Texture, u_Sampler), v_Uv); } diff --git a/wgpu/src/shader/image.frag.spv b/wgpu/src/shader/image.frag.spv Binary files differindex ebee82ac..65b08aa3 100644 --- a/wgpu/src/shader/image.frag.spv +++ b/wgpu/src/shader/image.frag.spv diff --git a/wgpu/src/shader/image.vert b/wgpu/src/shader/image.vert index 688c2311..dab53cfe 100644 --- a/wgpu/src/shader/image.vert +++ b/wgpu/src/shader/image.vert @@ -3,15 +3,18 @@ layout(location = 0) in vec2 v_Pos; layout(location = 1) in vec2 i_Pos; layout(location = 2) in vec2 i_Scale; +layout(location = 3) in vec2 i_Atlas_Pos; +layout(location = 4) in vec2 i_Atlas_Scale; +layout(location = 5) in uint i_Layer; layout (set = 0, binding = 0) uniform Globals { mat4 u_Transform; }; -layout(location = 0) out vec2 o_Uv; +layout(location = 0) out vec3 o_Uv; void main() { - o_Uv = v_Pos; + o_Uv = vec3(v_Pos * i_Atlas_Scale + i_Atlas_Pos, i_Layer); mat4 i_Transform = mat4( vec4(i_Scale.x, 0.0, 0.0, 0.0), diff --git a/wgpu/src/shader/image.vert.spv b/wgpu/src/shader/image.vert.spv Binary files differindex 9ba702bc..21f5db2d 100644 --- a/wgpu/src/shader/image.vert.spv +++ b/wgpu/src/shader/image.vert.spv diff --git a/wgpu/src/shader/quad.frag b/wgpu/src/shader/quad.frag index 2ee77e71..ad1af1ad 100644 --- a/wgpu/src/shader/quad.frag +++ b/wgpu/src/shader/quad.frag @@ -1,14 +1,17 @@ #version 450 layout(location = 0) in vec4 v_Color; -layout(location = 1) in vec2 v_Pos; -layout(location = 2) in vec2 v_Scale; -layout(location = 3) in float v_BorderRadius; +layout(location = 1) in vec4 v_BorderColor; +layout(location = 2) in vec2 v_Pos; +layout(location = 3) in vec2 v_Scale; +layout(location = 4) in float v_BorderRadius; +layout(location = 5) in float v_BorderWidth; layout(location = 0) out vec4 o_Color; -float rounded(in vec2 frag_coord, in vec2 position, in vec2 size, float radius, float s) +float distance(in vec2 frag_coord, in vec2 position, in vec2 size, float radius) { + // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN vec2 inner_size = size - vec2(radius, radius) * 2.0; vec2 top_left = position + vec2(radius, radius); vec2 bottom_right = top_left + inner_size; @@ -21,13 +24,43 @@ float rounded(in vec2 frag_coord, in vec2 position, in vec2 size, float radius, max(max(top_left_distance.y, bottom_right_distance.y), 0) ); - float d = sqrt(distance.x * distance.x + distance.y * distance.y); - - return 1.0 - smoothstep(radius - s, radius + s, d); + return sqrt(distance.x * distance.x + distance.y * distance.y); } void main() { - float radius_alpha = rounded(gl_FragCoord.xy, v_Pos, v_Scale, v_BorderRadius, 0.5); + vec4 mixed_color; + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0); + + float internal_distance = distance( + gl_FragCoord.xy, + v_Pos + vec2(v_BorderWidth), + v_Scale - vec2(v_BorderWidth * 2.0), + internal_border + ); + + float border_mix = smoothstep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(v_Color, v_BorderColor, border_mix); + } else { + mixed_color = v_Color; + } + + float d = distance( + gl_FragCoord.xy, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0), v_BorderRadius + 0.5, d); - o_Color = vec4(v_Color.xyz, v_Color.w * radius_alpha); + o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); } diff --git a/wgpu/src/shader/quad.frag.spv b/wgpu/src/shader/quad.frag.spv Binary files differindex 17bd8f46..519f5f01 100644 --- a/wgpu/src/shader/quad.frag.spv +++ b/wgpu/src/shader/quad.frag.spv diff --git a/wgpu/src/shader/quad.vert b/wgpu/src/shader/quad.vert index 539755cb..1d9a4fd2 100644 --- a/wgpu/src/shader/quad.vert +++ b/wgpu/src/shader/quad.vert @@ -4,7 +4,9 @@ layout(location = 0) in vec2 v_Pos; layout(location = 1) in vec2 i_Pos; layout(location = 2) in vec2 i_Scale; layout(location = 3) in vec4 i_Color; -layout(location = 4) in float i_BorderRadius; +layout(location = 4) in vec4 i_BorderColor; +layout(location = 5) in float i_BorderRadius; +layout(location = 6) in float i_BorderWidth; layout (set = 0, binding = 0) uniform Globals { mat4 u_Transform; @@ -12,9 +14,11 @@ layout (set = 0, binding = 0) uniform Globals { }; layout(location = 0) out vec4 o_Color; -layout(location = 1) out vec2 o_Pos; -layout(location = 2) out vec2 o_Scale; -layout(location = 3) out float o_BorderRadius; +layout(location = 1) out vec4 o_BorderColor; +layout(location = 2) out vec2 o_Pos; +layout(location = 3) out vec2 o_Scale; +layout(location = 4) out float o_BorderRadius; +layout(location = 5) out float o_BorderWidth; void main() { vec2 p_Pos = i_Pos * u_Scale; @@ -28,9 +32,11 @@ void main() { ); o_Color = i_Color; + o_BorderColor = i_BorderColor; o_Pos = p_Pos; o_Scale = p_Scale; o_BorderRadius = i_BorderRadius * u_Scale; + o_BorderWidth = i_BorderWidth * u_Scale; gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0); } diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv Binary files differindex 9050adfb..7059b51b 100644 --- a/wgpu/src/shader/quad.vert.spv +++ b/wgpu/src/shader/quad.vert.spv diff --git a/wgpu/src/shader/triangle.frag b/wgpu/src/shader/triangle.frag new file mode 100644 index 00000000..e39c45e7 --- /dev/null +++ b/wgpu/src/shader/triangle.frag @@ -0,0 +1,8 @@ +#version 450 + +layout(location = 0) in vec4 i_Color; +layout(location = 0) out vec4 o_Color; + +void main() { + o_Color = i_Color; +} diff --git a/wgpu/src/shader/triangle.frag.spv b/wgpu/src/shader/triangle.frag.spv Binary files differnew file mode 100644 index 00000000..11201872 --- /dev/null +++ b/wgpu/src/shader/triangle.frag.spv diff --git a/wgpu/src/shader/triangle.vert b/wgpu/src/shader/triangle.vert new file mode 100644 index 00000000..1f2c009b --- /dev/null +++ b/wgpu/src/shader/triangle.vert @@ -0,0 +1,15 @@ +#version 450 + +layout(location = 0) in vec2 i_Position; +layout(location = 1) in vec4 i_Color; + +layout(location = 0) out vec4 o_Color; + +layout (set = 0, binding = 0) uniform Globals { + mat4 u_Transform; +}; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + o_Color = i_Color; +} diff --git a/wgpu/src/shader/triangle.vert.spv b/wgpu/src/shader/triangle.vert.spv Binary files differnew file mode 100644 index 00000000..871f4f55 --- /dev/null +++ b/wgpu/src/shader/triangle.vert.spv diff --git a/wgpu/src/target.rs b/wgpu/src/target.rs new file mode 100644 index 00000000..1e72c0c3 --- /dev/null +++ b/wgpu/src/target.rs @@ -0,0 +1,14 @@ +use crate::Viewport; + +/// A rendering target. +#[derive(Debug)] +pub struct Target<'a> { + /// The texture where graphics will be rendered. + pub texture: &'a wgpu::TextureView, + + /// The viewport of the target. + /// + /// Most of the time, you will want this to match the dimensions of the + /// texture. + pub viewport: &'a Viewport, +} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index bf37abe5..c5670102 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -11,6 +11,8 @@ pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External { pub const CHECKMARK_ICON: char = '\u{F00C}'; +const FALLBACK_FONT: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); + #[derive(Debug)] pub struct Pipeline { draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>, @@ -20,29 +22,43 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &mut wgpu::Device) -> Self { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + default_font: Option<&[u8]>, + ) -> Self { // TODO: Font customization let font_source = font::Source::new(); - let default_font = font_source - .load(&[font::Family::SansSerif, font::Family::Serif]) - .expect("Find sans-serif or serif font"); + let default_font = + default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| { + font_source + .load(&[font::Family::SansSerif, font::Family::Serif]) + .unwrap_or_else(|_| FALLBACK_FONT.to_vec()) + }); + + let load_glyph_brush = |font: Vec<u8>| { + let builder = + wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ + font.clone() + ])?; + + Ok(( + builder, + glyph_brush::GlyphBrushBuilder::using_font_bytes(font).build(), + )) + }; - let mono_font = font_source - .load(&[font::Family::Monospace]) - .expect("Find monospace font"); + let (brush_builder, measure_brush) = load_glyph_brush(default_font) + .unwrap_or_else(|_: wgpu_glyph::rusttype::Error| { + log::warn!("System font failed to load. Falling back to embedded font..."); - let draw_brush = - wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ - mono_font, - default_font.clone(), - ]) - .initial_cache_size((2048, 2048)) - .build(device, wgpu::TextureFormat::Bgra8UnormSrgb); + load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font") + }); - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font_bytes(default_font) - .build(); + let draw_brush = brush_builder + .initial_cache_size((2048, 2048)) + .build(device, format); Pipeline { draw_brush: RefCell::new(draw_brush), @@ -95,14 +111,7 @@ impl Pipeline { text: content, scale: wgpu_glyph::Scale { x: size, y: size }, bounds: (bounds.width, bounds.height), - - // TODO: This is a bit hacky. We are loading the debug font as the - // first font in the `draw_brush`. The `measure_brush` does not - // contain this, hence we subtract 1. - // - // This should go away once we unify `draw_brush` and - // `measure_brush`. - font_id: wgpu_glyph::FontId(font_id - 1), + font_id: wgpu_glyph::FontId(font_id), ..Default::default() }; @@ -134,16 +143,28 @@ impl Pipeline { // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. // This makes stuff quite inconvenient. A manual method for trimming the // cache would make our lives easier. - let _ = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}) - .expect("Trim text measurements"); + loop { + let action = self + .measure_brush + .borrow_mut() + .process_queued(|_, _| {}, |_| {}); + + match action { + Ok(_) => break, + Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { + let (width, height) = suggested; + + self.measure_brush + .borrow_mut() + .resize_texture(width, height); + } + } + } } pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId { match font { - iced_native::Font::Default => wgpu_glyph::FontId(1), + iced_native::Font::Default => wgpu_glyph::FontId(0), iced_native::Font::External { name, bytes } => { if let Some(font_id) = self.draw_font_map.borrow().get(name) { return *font_id; diff --git a/wgpu/src/text/font.rs b/wgpu/src/text/font.rs index 31df5bf4..7346ccdb 100644 --- a/wgpu/src/text/font.rs +++ b/wgpu/src/text/font.rs @@ -1,5 +1,6 @@ -pub use font_kit::error::SelectionError as LoadError; -pub use font_kit::family_name::FamilyName as Family; +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; pub struct Source { raw: font_kit::source::SystemSource, diff --git a/wgpu/src/transformation.rs b/wgpu/src/transformation.rs index c8a7ee75..666696f3 100644 --- a/wgpu/src/transformation.rs +++ b/wgpu/src/transformation.rs @@ -13,10 +13,10 @@ impl Transformation { /// Creates an orthographic projection. #[rustfmt::skip] - pub fn orthographic(width: u16, height: u16) -> Transformation { + pub fn orthographic(width: u32, height: u32) -> Transformation { Transformation(Mat4::from_cols( - Vec4::new(2.0 / f32::from(width), 0.0, 0.0, 0.0), - Vec4::new(0.0, 2.0 / f32::from(height), 0.0, 0.0), + Vec4::new(2.0 / width as f32, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0 / height as f32, 0.0, 0.0), Vec4::new(0.0, 0.0, -1.0, 0.0), Vec4::new(-1.0, -1.0, 0.0, 1.0) )) diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs new file mode 100644 index 00000000..85ed4bd5 --- /dev/null +++ b/wgpu/src/triangle.rs @@ -0,0 +1,391 @@ +//! Draw meshes of triangles. +use crate::{settings, Transformation}; +use iced_native::{Point, Rectangle}; +use std::{mem, sync::Arc}; + +mod msaa; + +const UNIFORM_BUFFER_SIZE: usize = 100; +const VERTEX_BUFFER_SIZE: usize = 100_000; +const INDEX_BUFFER_SIZE: usize = 100_000; + +#[derive(Debug)] +pub(crate) struct Pipeline { + pipeline: wgpu::RenderPipeline, + blit: Option<msaa::Blit>, + constants: wgpu::BindGroup, + uniforms_buffer: Buffer<Uniforms>, + vertex_buffer: Buffer<Vertex2D>, + index_buffer: Buffer<u32>, +} + +#[derive(Debug)] +struct Buffer<T> { + raw: wgpu::Buffer, + size: usize, + usage: wgpu::BufferUsage, + _type: std::marker::PhantomData<T>, +} + +impl<T> Buffer<T> { + pub fn new( + device: &wgpu::Device, + size: usize, + usage: wgpu::BufferUsage, + ) -> Self { + let raw = device.create_buffer(&wgpu::BufferDescriptor { + size: (std::mem::size_of::<T>() * size) as u64, + usage, + }); + + Buffer { + raw, + size, + usage, + _type: std::marker::PhantomData, + } + } + + pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) { + if self.size < size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + size: (std::mem::size_of::<T>() * size) as u64, + usage: self.usage, + }); + + self.size = size; + } + } +} + +impl Pipeline { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option<settings::Antialiasing>, + ) -> Pipeline { + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::VERTEX, + ty: wgpu::BindingType::UniformBuffer { dynamic: true }, + }], + }); + + let constants_buffer = Buffer::new( + device, + UNIFORM_BUFFER_SIZE, + wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + ); + + let constant_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &constant_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Buffer { + buffer: &constants_buffer.raw, + range: 0..std::mem::size_of::<Uniforms>() as u64, + }, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&constant_layout], + }); + + let vs = include_bytes!("shader/triangle.vert.spv"); + let vs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) + .expect("Read triangle vertex shader as SPIR-V"), + ); + + let fs = include_bytes!("shader/triangle.frag.spv"); + let fs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) + .expect("Read triangle fragment shader as SPIR-V"), + ); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Cw, + cull_mode: wgpu::CullMode::None, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format, + color_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint32, + vertex_buffers: &[wgpu::VertexBufferDescriptor { + stride: mem::size_of::<Vertex2D>() as u64, + step_mode: wgpu::InputStepMode::Vertex, + attributes: &[ + // Position + wgpu::VertexAttributeDescriptor { + shader_location: 0, + format: wgpu::VertexFormat::Float2, + offset: 0, + }, + // Color + wgpu::VertexAttributeDescriptor { + shader_location: 1, + format: wgpu::VertexFormat::Float4, + offset: 4 * 2, + }, + ], + }], + sample_count: antialiasing + .map(|a| a.sample_count()) + .unwrap_or(1), + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Pipeline { + pipeline, + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + constants: constant_bind_group, + uniforms_buffer: constants_buffer, + vertex_buffer: Buffer::new( + device, + VERTEX_BUFFER_SIZE, + wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, + ), + index_buffer: Buffer::new( + device, + INDEX_BUFFER_SIZE, + wgpu::BufferUsage::INDEX | wgpu::BufferUsage::COPY_DST, + ), + } + } + + pub fn draw( + &mut self, + device: &mut wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_width: u32, + target_height: u32, + transformation: Transformation, + meshes: &Vec<(Point, Arc<Mesh2D>)>, + bounds: Rectangle<u32>, + ) { + // This looks a bit crazy, but we are just counting how many vertices + // and indices we will need to handle. + // TODO: Improve readability + let (total_vertices, total_indices) = meshes + .iter() + .map(|(_, mesh)| (mesh.vertices.len(), mesh.indices.len())) + .fold((0, 0), |(total_v, total_i), (v, i)| { + (total_v + v, total_i + i) + }); + + // Then we ensure the current buffers are big enough, resizing if + // necessary + self.uniforms_buffer.ensure_capacity(device, meshes.len()); + self.vertex_buffer.ensure_capacity(device, total_vertices); + self.index_buffer.ensure_capacity(device, total_indices); + + let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len()); + let mut offsets: Vec<( + wgpu::BufferAddress, + wgpu::BufferAddress, + usize, + )> = Vec::with_capacity(meshes.len()); + let mut last_vertex = 0; + let mut last_index = 0; + + // We upload everything upfront + for (origin, mesh) in meshes { + let transform = Uniforms { + transform: (transformation + * Transformation::translate(origin.x, origin.y)) + .into(), + }; + + let vertex_buffer = device + .create_buffer_mapped( + mesh.vertices.len(), + wgpu::BufferUsage::COPY_SRC, + ) + .fill_from_slice(&mesh.vertices); + + let index_buffer = device + .create_buffer_mapped( + mesh.indices.len(), + wgpu::BufferUsage::COPY_SRC, + ) + .fill_from_slice(&mesh.indices); + + encoder.copy_buffer_to_buffer( + &vertex_buffer, + 0, + &self.vertex_buffer.raw, + (std::mem::size_of::<Vertex2D>() * last_vertex) as u64, + (std::mem::size_of::<Vertex2D>() * mesh.vertices.len()) as u64, + ); + + encoder.copy_buffer_to_buffer( + &index_buffer, + 0, + &self.index_buffer.raw, + (std::mem::size_of::<u32>() * last_index) as u64, + (std::mem::size_of::<u32>() * mesh.indices.len()) as u64, + ); + + uniforms.push(transform); + offsets.push(( + last_vertex as u64, + last_index as u64, + mesh.indices.len(), + )); + + last_vertex += mesh.vertices.len(); + last_index += mesh.indices.len(); + } + + let uniforms_buffer = device + .create_buffer_mapped(uniforms.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&uniforms); + + encoder.copy_buffer_to_buffer( + &uniforms_buffer, + 0, + &self.uniforms_buffer.raw, + 0, + (std::mem::size_of::<Uniforms>() * uniforms.len()) as u64, + ); + + { + let (attachment, resolve_target, load_op) = + if let Some(blit) = &mut self.blit { + let (attachment, resolve_target) = + blit.targets(device, target_width, target_height); + + (attachment, Some(resolve_target), wgpu::LoadOp::Clear) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment, + resolve_target, + load_op, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + }, + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + for (i, (vertex_offset, index_offset, indices)) in + offsets.into_iter().enumerate() + { + render_pass.set_bind_group( + 0, + &self.constants, + &[(std::mem::size_of::<Uniforms>() * i) as u64], + ); + + render_pass.set_index_buffer( + &self.index_buffer.raw, + index_offset * std::mem::size_of::<u32>() as u64, + ); + + render_pass.set_vertex_buffers( + 0, + &[( + &self.vertex_buffer.raw, + vertex_offset * std::mem::size_of::<Vertex2D>() as u64, + )], + ); + + render_pass.draw_indexed(0..indices as u32, 0, 0..1); + } + } + + if let Some(blit) = &mut self.blit { + blit.draw(encoder, target); + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], +} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + } + } +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +/// +/// [`Vertex2D`]: struct.Vertex2D.html +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec<Vertex2D>, + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec<u32>, +} diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs new file mode 100644 index 00000000..7ccfb062 --- /dev/null +++ b/wgpu/src/triangle/msaa.rs @@ -0,0 +1,264 @@ +use crate::settings; + +#[derive(Debug)] +pub struct Blit { + format: wgpu::TextureFormat, + pipeline: wgpu::RenderPipeline, + constants: wgpu::BindGroup, + texture_layout: wgpu::BindGroupLayout, + sample_count: u32, + targets: Option<Targets>, +} + +impl Blit { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: settings::Antialiasing, + ) -> Blit { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, + }); + + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler, + }], + }); + + let constant_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &constant_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + multisampled: false, + dimension: wgpu::TextureViewDimension::D2, + }, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&constant_layout, &texture_layout], + }); + + let vs = include_bytes!("../shader/blit.vert.spv"); + let vs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) + .expect("Read blit vertex shader as SPIR-V"), + ); + + let fs = include_bytes!("../shader/blit.frag.spv"); + let fs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) + .expect("Read blit fragment shader as SPIR-V"), + ); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Cw, + cull_mode: wgpu::CullMode::None, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format, + color_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Blit { + format, + pipeline, + constants: constant_bind_group, + texture_layout: texture_layout, + sample_count: antialiasing.sample_count(), + targets: None, + } + } + + pub fn targets( + &mut self, + device: &wgpu::Device, + width: u32, + height: u32, + ) -> (&wgpu::TextureView, &wgpu::TextureView) { + match &mut self.targets { + None => { + self.targets = Some(Targets::new( + &device, + self.format, + &self.texture_layout, + self.sample_count, + width, + height, + )); + } + Some(targets) => { + if targets.width != width || targets.height != height { + self.targets = Some(Targets::new( + &device, + self.format, + &self.texture_layout, + self.sample_count, + width, + height, + )); + } + } + } + + let targets = self.targets.as_ref().unwrap(); + + (&targets.attachment, &targets.resolve) + } + + pub fn draw( + &self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + load_op: wgpu::LoadOp::Load, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + }, + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group( + 1, + &self.targets.as_ref().unwrap().bind_group, + &[], + ); + render_pass.draw(0..6, 0..1); + } +} + +#[derive(Debug)] +struct Targets { + attachment: wgpu::TextureView, + resolve: wgpu::TextureView, + bind_group: wgpu::BindGroup, + width: u32, + height: u32, +} + +impl Targets { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + texture_layout: &wgpu::BindGroupLayout, + sample_count: u32, + width: u32, + height: u32, + ) -> Targets { + let extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + + let attachment = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 1, + mip_level_count: 1, + sample_count, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + }); + + let resolve = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT + | wgpu::TextureUsage::SAMPLED, + }); + + let attachment = attachment.create_default_view(); + let resolve = resolve.create_default_view(); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(&resolve), + }], + }); + + Targets { + attachment, + resolve, + bind_group, + width, + height, + } + } +} diff --git a/wgpu/src/viewport.rs b/wgpu/src/viewport.rs new file mode 100644 index 00000000..66242468 --- /dev/null +++ b/wgpu/src/viewport.rs @@ -0,0 +1,29 @@ +use crate::Transformation; + +/// A viewing region for displaying computer graphics. +#[derive(Debug)] +pub struct Viewport { + width: u32, + height: u32, + transformation: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given dimensions. + pub fn new(width: u32, height: u32) -> Viewport { + Viewport { + width, + height, + transformation: Transformation::orthographic(width, height), + } + } + + /// Returns the dimensions of the [`Viewport`]. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + pub(crate) fn transformation(&self) -> Transformation { + self.transformation + } +} diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs new file mode 100644 index 00000000..73cce7e2 --- /dev/null +++ b/wgpu/src/widget.rs @@ -0,0 +1,41 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_wgpu::{button, Button}; +//! ``` +pub mod button; +pub mod checkbox; +pub mod container; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +#[cfg(feature = "canvas")] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs new file mode 100644 index 00000000..b738c55e --- /dev/null +++ b/wgpu/src/widget/button.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::button::State; +pub use iced_style::button::{Style, StyleSheet}; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs new file mode 100644 index 00000000..38c1ce62 --- /dev/null +++ b/wgpu/src/widget/canvas.rs @@ -0,0 +1,149 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +use crate::{Defaults, Primitive, Renderer}; + +use iced_native::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget, +}; +use std::hash::Hash; + +pub mod layer; +pub mod path; + +mod drawable; +mod fill; +mod frame; +mod stroke; + +pub use drawable::Drawable; +pub use fill::Fill; +pub use frame::Frame; +pub use layer::Layer; +pub use path::Path; +pub use stroke::{LineCap, LineJoin, Stroke}; + +/// A widget capable of drawing 2D graphics. +/// +/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the +/// painter's algorithm. In other words, layers will be drawn on top of each in +/// the same order they are pushed into the [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Layer`]: layer/trait.Layer.html +#[derive(Debug)] +pub struct Canvas<'a> { + width: Length, + height: Length, + layers: Vec<Box<dyn Layer + 'a>>, +} + +impl<'a> Canvas<'a> { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`] with no layers. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn new() -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + layers: Vec::new(), + } + } + + /// Sets the width of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Adds a [`Layer`] to the [`Canvas`]. + /// + /// It will be drawn on top of previous layers. + /// + /// [`Layer`]: layer/trait.Layer.html + /// [`Canvas`]: struct.Canvas.html + pub fn push(mut self, layer: impl Layer + 'a) -> Self { + self.layers.push(Box::new(layer)); + self + } +} + +impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> { + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let bounds = layout.bounds(); + let origin = Point::new(bounds.x, bounds.y); + let size = Size::new(bounds.width, bounds.height); + + ( + Primitive::Group { + primitives: self + .layers + .iter() + .map(|layer| Primitive::Mesh2D { + origin, + buffers: layer.draw(size), + }) + .collect(), + }, + MouseCursor::Idle, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + std::any::TypeId::of::<Canvas<'static>>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message> From<Canvas<'a>> for Element<'a, Message, Renderer> +where + Message: 'static, +{ + fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/wgpu/src/widget/canvas/drawable.rs b/wgpu/src/widget/canvas/drawable.rs new file mode 100644 index 00000000..6c74071c --- /dev/null +++ b/wgpu/src/widget/canvas/drawable.rs @@ -0,0 +1,12 @@ +use crate::canvas::Frame; + +/// A type that can be drawn on a [`Frame`]. +/// +/// [`Frame`]: struct.Frame.html +pub trait Drawable { + /// Draws the [`Drawable`] on the given [`Frame`]. + /// + /// [`Drawable`]: trait.Drawable.html + /// [`Frame`]: struct.Frame.html + fn draw(&self, frame: &mut Frame); +} diff --git a/wgpu/src/widget/canvas/fill.rs b/wgpu/src/widget/canvas/fill.rs new file mode 100644 index 00000000..5ce24cf3 --- /dev/null +++ b/wgpu/src/widget/canvas/fill.rs @@ -0,0 +1,14 @@ +use iced_native::Color; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub enum Fill { + /// Fill with a color. + Color(Color), +} + +impl Default for Fill { + fn default() -> Fill { + Fill::Color(Color::BLACK) + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs new file mode 100644 index 00000000..fa6d8c0a --- /dev/null +++ b/wgpu/src/widget/canvas/frame.rs @@ -0,0 +1,255 @@ +use iced_native::{Point, Size, Vector}; + +use crate::{ + canvas::{Fill, Path, Stroke}, + triangle, +}; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +#[derive(Debug)] +pub struct Frame { + width: f32, + height: f32, + buffers: lyon::tessellation::VertexBuffers<triangle::Vertex2D, u32>, + transforms: Transforms, +} + +#[derive(Debug)] +struct Transforms { + previous: Vec<Transform>, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + /// + /// [`Frame`]: struct.Frame.html + pub fn new(width: f32, height: f32) -> Frame { + Frame { + width, + height, + buffers: lyon::tessellation::VertexBuffers::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + } + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn width(&self) -> f32 { + self.width + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn height(&self) -> f32 { + self.height + } + + /// Returns the dimensions of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + /// Returns the coordinate of the center of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn center(&self) -> Point { + Point::new(self.width / 2.0, self.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn fill(&mut self, path: &Path, fill: Fill) { + use lyon::tessellation::{ + BuffersBuilder, FillOptions, FillTessellator, + }; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(match fill { + Fill::Color(color) => color.into_linear(), + }), + ); + + let mut tessellator = FillTessellator::new(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + }; + + let _ = result.expect("Tessellate path"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn stroke(&mut self, path: &Path, stroke: Stroke) { + use lyon::tessellation::{ + BuffersBuilder, StrokeOptions, StrokeTessellator, + }; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + StrokeVertex(stroke.color.into_linear()), + ); + + let mut tessellator = StrokeTessellator::new(); + + let mut options = StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = stroke.line_cap.into(); + options.end_cap = stroke.line_cap.into(); + options.line_join = stroke.line_join.into(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + }; + + let _ = result.expect("Stroke path"); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.transforms.previous.push(self.transforms.current); + + f(self); + + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Applies a translation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(-angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the geometry that has been drawn on the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + pub fn into_mesh(self) -> triangle::Mesh2D { + triangle::Mesh2D { + vertices: self.buffers.vertices, + indices: self.buffers.indices, + } + } +} + +struct FillVertex([f32; 4]); + +impl lyon::tessellation::FillVertexConstructor<triangle::Vertex2D> + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::FillAttributes<'_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +struct StrokeVertex([f32; 4]); + +impl lyon::tessellation::StrokeVertexConstructor<triangle::Vertex2D> + for StrokeVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::StrokeAttributes<'_, '_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs new file mode 100644 index 00000000..82d647bb --- /dev/null +++ b/wgpu/src/widget/canvas/layer.rs @@ -0,0 +1,25 @@ +//! Produce, store, and reuse geometry. +mod cache; + +pub use cache::Cache; + +use crate::triangle; + +use iced_native::Size; +use std::sync::Arc; + +/// A layer that can be presented at a [`Canvas`]. +/// +/// [`Canvas`]: ../struct.Canvas.html +pub trait Layer: std::fmt::Debug { + /// Draws the [`Layer`] in the given bounds and produces [`Mesh2D`] as a + /// result. + /// + /// The [`Layer`] may choose to store the produced [`Mesh2D`] locally and + /// only recompute it when the bounds change, its contents change, or is + /// otherwise explicitly cleared by other means. + /// + /// [`Layer`]: trait.Layer.html + /// [`Mesh2D`]: ../../../triangle/struct.Mesh2D.html + fn draw(&self, bounds: Size) -> Arc<triangle::Mesh2D>; +} diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs new file mode 100644 index 00000000..3071cce0 --- /dev/null +++ b/wgpu/src/widget/canvas/layer/cache.rs @@ -0,0 +1,101 @@ +use crate::{ + canvas::{Drawable, Frame, Layer}, + triangle, +}; + +use iced_native::Size; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::sync::Arc; + +/// A simple cache that stores generated geometry to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +/// +/// [`Layer`]: ../trait.Layer.html +/// [`Cached`]: struct.Cached.html +#[derive(Debug)] +pub struct Cache<T: Drawable> { + input: PhantomData<T>, + state: RefCell<State>, +} + +#[derive(Debug)] +enum State { + Empty, + Filled { + mesh: Arc<triangle::Mesh2D>, + bounds: Size, + }, +} + +impl<T> Cache<T> +where + T: Drawable + std::fmt::Debug, +{ + /// Creates a new empty [`Cache`]. + /// + /// [`Cache`]: struct.Cache.html + pub fn new() -> Self { + Cache { + input: PhantomData, + state: RefCell::new(State::Empty), + } + } + + /// Clears the cache, forcing a redraw the next time it is used. + /// + /// [`Cached`]: struct.Cached.html + pub fn clear(&mut self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Binds the [`Cache`] with some data, producing a [`Layer`] that can be + /// added to a [`Canvas`]. + /// + /// [`Cache`]: struct.Cache.html + /// [`Layer`]: ../trait.Layer.html + /// [`Canvas`]: ../../struct.Canvas.html + pub fn with<'a>(&'a self, input: &'a T) -> impl Layer + 'a { + Bind { + cache: self, + input: input, + } + } +} + +#[derive(Debug)] +struct Bind<'a, T: Drawable> { + cache: &'a Cache<T>, + input: &'a T, +} + +impl<'a, T> Layer for Bind<'a, T> +where + T: Drawable + std::fmt::Debug, +{ + fn draw(&self, current_bounds: Size) -> Arc<triangle::Mesh2D> { + use std::ops::Deref; + + if let State::Filled { mesh, bounds } = + self.cache.state.borrow().deref() + { + if *bounds == current_bounds { + return mesh.clone(); + } + } + + let mut frame = Frame::new(current_bounds.width, current_bounds.height); + self.input.draw(&mut frame); + + let mesh = Arc::new(frame.into_mesh()); + + *self.cache.state.borrow_mut() = State::Filled { + mesh: mesh.clone(), + bounds: current_bounds, + }; + + mesh + } +} diff --git a/wgpu/src/widget/canvas/path.rs b/wgpu/src/widget/canvas/path.rs new file mode 100644 index 00000000..15c2e853 --- /dev/null +++ b/wgpu/src/widget/canvas/path.rs @@ -0,0 +1,49 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +pub use arc::Arc; +pub use builder::Builder; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +/// +/// [`Path`]: struct.Path.html +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon::path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + #[inline] + pub(crate) fn raw(&self) -> &lyon::path::Path { + &self.raw + } + + #[inline] + pub(crate) fn transformed( + &self, + transform: &lyon::math::Transform, + ) -> Path { + Path { + raw: self.raw.transformed(transform), + } + } +} diff --git a/wgpu/src/widget/canvas/path/arc.rs b/wgpu/src/widget/canvas/path/arc.rs new file mode 100644 index 00000000..343191f1 --- /dev/null +++ b/wgpu/src/widget/canvas/path/arc.rs @@ -0,0 +1,44 @@ +//! Build and draw curves. +use iced_native::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +/// +/// [`Arc`]: struct.Arc.html +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From<Arc> for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/wgpu/src/widget/canvas/path/builder.rs b/wgpu/src/widget/canvas/path/builder.rs new file mode 100644 index 00000000..a013149e --- /dev/null +++ b/wgpu/src/widget/canvas/path/builder.rs @@ -0,0 +1,177 @@ +use crate::canvas::path::{arc, Arc, Path}; + +use iced_native::{Point, Size}; +use lyon::path::builder::{Build, FlatPathBuilder, PathBuilder, SvgBuilder}; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +/// +/// [`Path`]: struct.Path.html +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: lyon::path::builder::SvgPathBuilder<lyon::path::Builder>, +} + +impl Builder { + /// Creates a new [`Builder`]. + /// + /// [`Builder`]: struct.Builder.html + pub fn new() -> Builder { + Builder { + raw: lyon::path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + /// + /// [`Arc`]: struct.Arc.html + /// [`Path`]: struct.Path.html + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// The arc is connected to the previous point by a straight line, if + /// necessary. + /// + /// [`Path`]: struct.Path.html + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + use lyon::{math, path}; + + let a = math::Point::new(a.x, a.y); + + if self.raw.current_position() != a { + let _ = self.raw.line_to(a); + } + + let _ = self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + path::ArcFlags::default(), + math::Point::new(b.x, b.y), + ); + } + + /// Adds an [`Ellipse`] to the [`Path`] using a clockwise direction. + /// + /// [`Ellipse`]: struct.Arc.html + /// [`Path`]: struct.Path.html + pub fn ellipse(&mut self, arc: arc::Elliptical) { + use lyon::{geom, math}; + + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + use lyon::math; + + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + use lyon::math; + + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn rectangle(&mut self, p: Point, size: Size) { + self.move_to(p); + self.line_to(Point::new(p.x + size.width, p.y)); + self.line_to(Point::new(p.x + size.width, p.y + size.height)); + self.line_to(Point::new(p.x, p.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} diff --git a/wgpu/src/widget/canvas/stroke.rs b/wgpu/src/widget/canvas/stroke.rs new file mode 100644 index 00000000..46d669c4 --- /dev/null +++ b/wgpu/src/widget/canvas/stroke.rs @@ -0,0 +1,83 @@ +use iced_native::Color; + +/// The style of a stroke. +#[derive(Debug, Clone, Copy)] +pub struct Stroke { + /// The color of the stroke. + pub color: Color, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, +} + +impl Default for Stroke { + fn default() -> Stroke { + Stroke { + color: Color::BLACK, + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +impl From<LineCap> for lyon::tessellation::LineCap { + fn from(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +impl From<LineJoin> for lyon::tessellation::LineJoin { + fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } + } +} diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs new file mode 100644 index 00000000..da0d7a84 --- /dev/null +++ b/wgpu/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_style::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>; diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs new file mode 100644 index 00000000..9a93a246 --- /dev/null +++ b/wgpu/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>; diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs new file mode 100644 index 00000000..34450b5e --- /dev/null +++ b/wgpu/src/widget/progress_bar.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_style::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar = iced_native::ProgressBar<Renderer>; diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs new file mode 100644 index 00000000..6e5cf042 --- /dev/null +++ b/wgpu/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_style::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message> = iced_native::Radio<Message, Renderer>; diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs new file mode 100644 index 00000000..1d236105 --- /dev/null +++ b/wgpu/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs new file mode 100644 index 00000000..4e47978f --- /dev/null +++ b/wgpu/src/widget/slider.rs @@ -0,0 +1,16 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::slider::State; +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Renderer>; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs new file mode 100644 index 00000000..260fe3a6 --- /dev/null +++ b/wgpu/src/widget/text_input.rs @@ -0,0 +1,15 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>; diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs new file mode 100644 index 00000000..b7adad82 --- /dev/null +++ b/wgpu/src/window.rs @@ -0,0 +1,6 @@ +//! Display rendering results on windows. +mod backend; +mod swap_chain; + +pub use backend::Backend; +pub use swap_chain::SwapChain; diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs new file mode 100644 index 00000000..5b269f36 --- /dev/null +++ b/wgpu/src/window/backend.rs @@ -0,0 +1,113 @@ +use crate::{window::SwapChain, Renderer, Settings, Target}; + +use iced_native::MouseCursor; +use raw_window_handle::HasRawWindowHandle; + +/// A window graphics backend for iced powered by `wgpu`. +#[derive(Debug)] +pub struct Backend { + device: wgpu::Device, + queue: wgpu::Queue, + format: wgpu::TextureFormat, +} + +impl iced_native::window::Backend for Backend { + type Settings = Settings; + type Renderer = Renderer; + type Surface = wgpu::Surface; + type SwapChain = SwapChain; + + fn new(settings: Self::Settings) -> (Backend, Renderer) { + let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions { + power_preference: if settings.antialiasing.is_none() { + wgpu::PowerPreference::Default + } else { + wgpu::PowerPreference::HighPerformance + }, + backends: wgpu::BackendBit::all(), + }) + .expect("Request adapter"); + + let (mut device, queue) = + adapter.request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: wgpu::Limits { max_bind_groups: 2 }, + }); + + let renderer = Renderer::new(&mut device, settings); + + ( + Backend { + device, + queue, + format: settings.format, + }, + renderer, + ) + } + + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> wgpu::Surface { + wgpu::Surface::create(window) + } + + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> SwapChain { + SwapChain::new(&self.device, surface, self.format, width, height) + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut SwapChain, + output: &<Self::Renderer as iced_native::Renderer>::Output, + scale_factor: f64, + overlay: &[T], + ) -> MouseCursor { + let (frame, viewport) = swap_chain.next_frame(); + + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { todo: 0 }, + ); + + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }, + }], + depth_stencil_attachment: None, + }); + + let mouse_cursor = renderer.draw( + &mut self.device, + &mut encoder, + Target { + texture: &frame.view, + viewport, + }, + output, + scale_factor, + overlay, + ); + + self.queue.submit(&[encoder.finish()]); + + mouse_cursor + } +} diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs new file mode 100644 index 00000000..4ca2901b --- /dev/null +++ b/wgpu/src/window/swap_chain.rs @@ -0,0 +1,57 @@ +use crate::Viewport; + +/// The rendering target of a window. +/// +/// It represents a series of virtual framebuffers with a scale factor. +#[derive(Debug)] +pub struct SwapChain { + raw: wgpu::SwapChain, + viewport: Viewport, +} + +impl SwapChain {} + +impl SwapChain { + /// Creates a new [`SwapChain`] for the given surface. + /// + /// [`SwapChain`]: struct.SwapChain.html + pub fn new( + device: &wgpu::Device, + surface: &wgpu::Surface, + format: wgpu::TextureFormat, + width: u32, + height: u32, + ) -> SwapChain { + SwapChain { + raw: new_swap_chain(surface, format, width, height, device), + viewport: Viewport::new(width, height), + } + } + + /// Returns the next frame of the [`SwapChain`] alongside its [`Viewport`]. + /// + /// [`SwapChain`]: struct.SwapChain.html + /// [`Viewport`]: ../struct.Viewport.html + pub fn next_frame(&mut self) -> (wgpu::SwapChainOutput<'_>, &Viewport) { + (self.raw.get_next_texture(), &self.viewport) + } +} + +fn new_swap_chain( + surface: &wgpu::Surface, + format: wgpu::TextureFormat, + width: u32, + height: u32, + device: &wgpu::Device, +) -> wgpu::SwapChain { + device.create_swap_chain( + &surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format, + width, + height, + present_mode: wgpu::PresentMode::Vsync, + }, + ) +} diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 2a33255d..63df1d63 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -6,12 +6,24 @@ edition = "2018" description = "A winit runtime for Iced" license = "MIT" repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_winit" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] [features] debug = [] [dependencies] -iced_native = { version = "0.1.0-alpha", path = "../native" } -winit = { version = "0.20.0-alpha3", git = "https://github.com/rust-windowing/winit", rev = "709808eb4e69044705fcb214bcc30556db761405"} -futures = { version = "0.3", features = ["thread-pool"] } +winit = "0.21" log = "0.4" + +[dependencies.iced_native] +version = "0.1.0-alpha" +path = "../native" + +[dependencies.window_clipboard] +git = "https://github.com/hecrj/window_clipboard" +rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf" + +[target.'cfg(target_os = "windows")'.dependencies.winapi] +version = "0.3.6" diff --git a/winit/README.md b/winit/README.md index 01785150..d3309e49 100644 --- a/winit/README.md +++ b/winit/README.md @@ -10,7 +10,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t  -[documentation]: https://docs.rs/iced_winit +[documentation]: https://docs.rs/iced_winit/0.1.0-alpha.1/iced_winit/ [`iced_native`]: ../native [`winit`]: https://github.com/rust-windowing/winit diff --git a/winit/src/application.rs b/winit/src/application.rs index 1042b412..f5aa799c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,9 +1,7 @@ use crate::{ - conversion, - input::{keyboard, mouse}, - renderer::{Target, Windowed}, - Cache, Command, Container, Debug, Element, Event, Length, MouseCursor, - Settings, UserInterface, + conversion, size::Size, window, Cache, Clipboard, Command, Debug, Element, + Executor, Mode, MouseCursor, Proxy, Runtime, Settings, Subscription, + UserInterface, }; /// An interactive, native cross-platform application. @@ -15,10 +13,15 @@ use crate::{ /// An [`Application`](trait.Application.html) can execute asynchronous actions /// by returning a [`Command`](struct.Command.html) in some of its methods. pub trait Application: Sized { - /// The renderer to use to draw the [`Application`]. + /// The graphics backend to use to draw the [`Application`]. /// /// [`Application`]: trait.Application.html - type Renderer: Windowed; + type Backend: window::Backend; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// [`Executor`]: trait.Executor.html + type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. /// @@ -57,12 +60,35 @@ pub trait Application: Sized { /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + fn subscription(&self) -> Subscription<Self::Message>; + /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. /// /// [`Application`]: trait.Application.html - fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; + fn view( + &mut self, + ) -> Element<'_, Self::Message, <Self::Backend as window::Backend>::Renderer>; + + /// Returns the current [`Application`] mode. + /// + /// The runtime will automatically transition your application if a new mode + /// is returned. + /// + /// By default, an application will run in windowed mode. + /// + /// [`Application`]: trait.Application.html + fn mode(&self) -> Mode { + Mode::Windowed + } /// Runs the [`Application`]. /// @@ -72,10 +98,13 @@ pub trait Application: Sized { /// It should probably be that last thing you call in your `main` function. /// /// [`Application`]: trait.Application.html - fn run(settings: Settings) - where + fn run( + settings: Settings, + backend_settings: <Self::Backend as window::Backend>::Settings, + ) where Self: 'static, { + use window::Backend as _; use winit::{ event::{self, WindowEvent}, event_loop::{ControlFlow, EventLoop}, @@ -86,49 +115,75 @@ pub trait Application: Sized { debug.startup_started(); let event_loop = EventLoop::with_user_event(); - let proxy = event_loop.create_proxy(); - let mut thread_pool = - futures::executor::ThreadPool::new().expect("Create thread pool"); let mut external_messages = Vec::new(); - let (mut application, init_command) = Self::new(); - spawn(init_command, &mut thread_pool, &proxy); + let mut runtime = { + let executor = Self::Executor::new().expect("Create executor"); + + Runtime::new(executor, Proxy::new(event_loop.create_proxy())) + }; + + let (mut application, init_command) = runtime.enter(|| Self::new()); + runtime.spawn(init_command); + + let subscription = application.subscription(); + runtime.track(subscription); let mut title = application.title(); + let mut mode = application.mode(); + + let window = { + let mut window_builder = WindowBuilder::new(); - let (width, height) = settings.window.size; + let (width, height) = settings.window.size; - let window = WindowBuilder::new() - .with_title(&title) - .with_inner_size(winit::dpi::LogicalSize { - width: f64::from(width), - height: f64::from(height), - }) - .with_resizable(settings.window.resizable) - .build(&event_loop) - .expect("Open window"); + window_builder = window_builder + .with_title(&title) + .with_inner_size(winit::dpi::LogicalSize { width, height }) + .with_resizable(settings.window.resizable) + .with_decorations(settings.window.decorations) + .with_fullscreen(conversion::fullscreen( + event_loop.primary_monitor(), + mode, + )); - let dpi = window.hidpi_factor(); - let mut size = window.inner_size(); - let mut new_size: Option<winit::dpi::LogicalSize> = None; + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; - let mut renderer = Self::Renderer::new(); + if let Some(parent) = settings.window.platform_specific.parent { + window_builder = window_builder.with_parent_window(parent); + } + } + + window_builder.build(&event_loop).expect("Open window") + }; - let mut target = { - let (width, height) = to_physical(size, dpi); + let mut size = Size::new(window.inner_size(), window.scale_factor()); + let mut resized = false; - <Self::Renderer as Windowed>::Target::new( - &window, width, height, dpi as f32, &renderer, + let clipboard = Clipboard::new(&window); + let (mut backend, mut renderer) = Self::Backend::new(backend_settings); + + let surface = backend.create_surface(&window); + + let mut swap_chain = { + let physical_size = size.physical(); + + backend.create_swap_chain( + &surface, + physical_size.width, + physical_size.height, ) }; - debug.layout_started(); - let user_interface = UserInterface::build( - document(&mut application, size, &mut debug), + let user_interface = build_user_interface( + &mut application, Cache::default(), &mut renderer, + size.logical(), + &mut debug, ); - debug.layout_finished(); debug.draw_started(); let mut primitive = user_interface.draw(&mut renderer); @@ -137,28 +192,43 @@ pub trait Application: Sized { let mut cache = Some(user_interface.into_cache()); let mut events = Vec::new(); let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut modifiers = winit::event::ModifiersState::default(); debug.startup_finished(); window.request_redraw(); event_loop.run(move |event, _, control_flow| match event { event::Event::MainEventsCleared => { + if events.is_empty() && external_messages.is_empty() { + return; + } + // TODO: We should be able to keep a user interface alive // between events once we remove state references. // // This will allow us to rebuild it only when a message is // handled. - debug.layout_started(); - let mut user_interface = UserInterface::build( - document(&mut application, size, &mut debug), + let mut user_interface = build_user_interface( + &mut application, cache.take().unwrap(), &mut renderer, + size.logical(), + &mut debug, ); - debug.layout_finished(); debug.event_processing_started(); - let mut messages = - user_interface.update(&renderer, events.drain(..)); + events + .iter() + .cloned() + .for_each(|event| runtime.broadcast(event)); + + let mut messages = user_interface.update( + events.drain(..), + clipboard + .as_ref() + .map(|c| c as &dyn iced_native::Clipboard), + &renderer, + ); messages.extend(external_messages.drain(..)); debug.event_processing_finished(); @@ -179,12 +249,15 @@ pub trait Application: Sized { debug.log_message(&message); debug.update_started(); - let command = application.update(message); - - spawn(command, &mut thread_pool, &proxy); + let command = + runtime.enter(|| application.update(message)); + runtime.spawn(command); debug.update_finished(); } + let subscription = application.subscription(); + runtime.track(subscription); + // Update window title let new_title = application.title(); @@ -194,13 +267,25 @@ pub trait Application: Sized { title = new_title; } - debug.layout_started(); - let user_interface = UserInterface::build( - document(&mut application, size, &mut debug), + // Update window mode + let new_mode = application.mode(); + + if mode != new_mode { + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + new_mode, + )); + + mode = new_mode; + } + + let user_interface = build_user_interface( + &mut application, temp_cache, &mut renderer, + size.logical(), + &mut debug, ); - debug.layout_finished(); debug.draw_started(); primitive = user_interface.draw(&mut renderer); @@ -217,22 +302,25 @@ pub trait Application: Sized { event::Event::RedrawRequested(_) => { debug.render_started(); - if let Some(new_size) = new_size.take() { - let dpi = window.hidpi_factor(); - let (width, height) = to_physical(new_size, dpi); + if resized { + let physical_size = size.physical(); - target.resize( - width, - height, - window.hidpi_factor() as f32, - &renderer, + swap_chain = backend.create_swap_chain( + &surface, + physical_size.width, + physical_size.height, ); - size = new_size; + resized = false; } - let new_mouse_cursor = - renderer.draw(&primitive, &debug.overlay(), &mut target); + let new_mouse_cursor = backend.draw( + &mut renderer, + &mut swap_chain, + &primitive, + size.scale_factor(), + &debug.overlay(), + ); debug.render_finished(); @@ -250,83 +338,56 @@ pub trait Application: Sized { event::Event::WindowEvent { event: window_event, .. - } => match window_event { - WindowEvent::CursorMoved { position, .. } => { - events.push(Event::Mouse(mouse::Event::CursorMoved { - x: position.x as f32, - y: position.y as f32, - })); - } - WindowEvent::MouseInput { button, state, .. } => { - events.push(Event::Mouse(mouse::Event::Input { - button: conversion::mouse_button(button), - state: conversion::button_state(state), - })); - } - WindowEvent::MouseWheel { delta, .. } => match delta { - winit::event::MouseScrollDelta::LineDelta( - delta_x, - delta_y, - ) => { - events.push(Event::Mouse( - mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { - x: delta_x, - y: delta_y, - }, - }, - )); + } => { + match window_event { + WindowEvent::Resized(new_size) => { + size = Size::new(new_size, window.scale_factor()); + resized = true; } - winit::event::MouseScrollDelta::PixelDelta(position) => { - events.push(Event::Mouse( - mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Pixels { - x: position.x as f32, - y: position.y as f32, - }, - }, - )); + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; } - }, - WindowEvent::ReceivedCharacter(c) - if !is_private_use_character(c) => - { - events.push(Event::Keyboard( - keyboard::Event::CharacterReceived(c), - )); - } - WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(virtual_keycode), - state, - .. - }, - .. - } => { - match (virtual_keycode, state) { - ( - winit::event::VirtualKeyCode::F12, - winit::event::ElementState::Pressed, - ) => debug.toggle(), - _ => {} + #[cfg(target_os = "macos")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: + Some(winit::event::VirtualKeyCode::Q), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if modifiers.logo() => { + *control_flow = ControlFlow::Exit; } - - events.push(Event::Keyboard(keyboard::Event::Input { - key_code: conversion::key_code(virtual_keycode), - state: conversion::button_state(state), - })); - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: + Some(winit::event::VirtualKeyCode::F12), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => debug.toggle(), + _ => {} } - WindowEvent::Resized(size) => { - new_size = Some(size.into()); - log::debug!("Resized: {:?}", new_size); + if let Some(event) = conversion::window_event( + window_event, + size.scale_factor(), + modifiers, + ) { + events.push(event); } - _ => {} - }, + } + event::Event::DeviceEvent { + event: event::DeviceEvent::ModifiersChanged(new_modifiers), + .. + } => { + modifiers = new_modifiers; + } _ => { *control_flow = ControlFlow::Wait; } @@ -334,63 +395,28 @@ pub trait Application: Sized { } } -fn to_physical(size: winit::dpi::LogicalSize, dpi: f64) -> (u16, u16) { - let physical_size = size.to_physical(dpi); - - ( - physical_size.width.round() as u16, - physical_size.height.round() as u16, - ) -} - -fn document<'a, Application>( - application: &'a mut Application, - size: winit::dpi::LogicalSize, +fn build_user_interface<'a, A: Application>( + application: &'a mut A, + cache: Cache, + renderer: &mut <A::Backend as window::Backend>::Renderer, + size: winit::dpi::LogicalSize<f64>, debug: &mut Debug, -) -> Element<'a, Application::Message, Application::Renderer> -where - Application: self::Application, - Application::Message: 'static, -{ +) -> UserInterface<'a, A::Message, <A::Backend as window::Backend>::Renderer> { debug.view_started(); let view = application.view(); debug.view_finished(); - Container::new(view) - .width(Length::Units(size.width.round() as u16)) - .height(Length::Units(size.height.round() as u16)) - .into() -} - -fn spawn<Message: Send>( - command: Command<Message>, - thread_pool: &mut futures::executor::ThreadPool, - proxy: &winit::event_loop::EventLoopProxy<Message>, -) { - use futures::FutureExt; - - let futures = command.futures(); - - for future in futures { - let proxy = proxy.clone(); - - let future = future.map(move |message| { - proxy - .send_event(message) - .expect("Send command result to event loop"); - }); - - thread_pool.spawn_ok(future); - } -} - -// As defined in: http://www.unicode.org/faq/private_use.html -// TODO: Remove once https://github.com/rust-windowing/winit/pull/1254 lands -fn is_private_use_character(c: char) -> bool { - match c { - '\u{E000}'..='\u{F8FF}' - | '\u{F0000}'..='\u{FFFFD}' - | '\u{100000}'..='\u{10FFFD}' => true, - _ => false, - } + debug.layout_started(); + let user_interface = UserInterface::build( + view, + iced_native::Size::new( + size.width.round() as f32, + size.height.round() as f32, + ), + cache, + renderer, + ); + debug.layout_finished(); + + user_interface } diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs new file mode 100644 index 00000000..1ff029ab --- /dev/null +++ b/winit/src/clipboard.rs @@ -0,0 +1,19 @@ +/// A buffer for short-term storage and transfer within and between +/// applications. +#[allow(missing_debug_implementations)] +pub struct Clipboard(window_clipboard::Clipboard); + +impl Clipboard { + /// Creates a new [`Clipboard`] for the given window. + /// + /// [`Clipboard`]: struct.Clipboard.html + pub fn new(window: &winit::window::Window) -> Option<Clipboard> { + window_clipboard::Clipboard::new(window).map(Clipboard).ok() + } +} + +impl iced_native::Clipboard for Clipboard { + fn content(&self) -> Option<String> { + self.0.read().ok() + } +} diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 03d583fb..b6a0b64b 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -3,11 +3,107 @@ //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native use crate::{ - input::{keyboard::KeyCode, mouse, ButtonState}, - MouseCursor, + input::{ + keyboard::{self, KeyCode, ModifiersState}, + mouse, ButtonState, + }, + window, Event, Mode, MouseCursor, }; -/// Convert a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon. +/// Converts a winit window event into an iced event. +pub fn window_event( + event: winit::event::WindowEvent<'_>, + scale_factor: f64, + modifiers: winit::event::ModifiersState, +) -> Option<Event> { + use winit::event::WindowEvent; + + match event { + WindowEvent::Resized(new_size) => { + let logical_size = new_size.to_logical(scale_factor); + + Some(Event::Window(window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + })) + } + WindowEvent::CursorMoved { position, .. } => { + let position = position.to_logical::<f64>(scale_factor); + + Some(Event::Mouse(mouse::Event::CursorMoved { + x: position.x as f32, + y: position.y as f32, + })) + } + WindowEvent::MouseInput { button, state, .. } => { + Some(Event::Mouse(mouse::Event::Input { + button: mouse_button(button), + state: button_state(state), + })) + } + WindowEvent::MouseWheel { delta, .. } => match delta { + winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => { + Some(Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { + x: delta_x, + y: delta_y, + }, + })) + } + winit::event::MouseScrollDelta::PixelDelta(position) => { + Some(Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { + x: position.x as f32, + y: position.y as f32, + }, + })) + } + }, + WindowEvent::ReceivedCharacter(c) if !is_private_use_character(c) => { + Some(Event::Keyboard(keyboard::Event::CharacterReceived(c))) + } + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(virtual_keycode), + state, + .. + }, + .. + } => Some(Event::Keyboard(keyboard::Event::Input { + key_code: key_code(virtual_keycode), + state: button_state(state), + modifiers: modifiers_state(modifiers), + })), + WindowEvent::HoveredFile(path) => { + Some(Event::Window(window::Event::FileHovered(path))) + } + WindowEvent::DroppedFile(path) => { + Some(Event::Window(window::Event::FileDropped(path))) + } + WindowEvent::HoveredFileCancelled => { + Some(Event::Window(window::Event::FilesHoveredLeft)) + } + _ => None, + } +} + +/// Converts a [`Mode`] to a [`winit`] fullscreen mode. +/// +/// [`Mode`]: +pub fn fullscreen( + monitor: winit::monitor::MonitorHandle, + mode: Mode, +) -> Option<winit::window::Fullscreen> { + match mode { + Mode::Windowed => None, + Mode::Fullscreen => { + Some(winit::window::Fullscreen::Borderless(monitor)) + } + } +} + +/// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native @@ -23,7 +119,7 @@ pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon { } } -/// Convert a `MouseButton` from [`winit`] to an [`iced_native`] mouse button. +/// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native @@ -36,7 +132,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { } } -/// Convert an `ElementState` from [`winit`] to an [`iced_native`] button state. +/// Converts an `ElementState` from [`winit`] to an [`iced_native`] button state. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native @@ -47,7 +143,23 @@ pub fn button_state(element_state: winit::event::ElementState) -> ButtonState { } } -/// Convert a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. +/// Converts some `ModifiersState` from [`winit`] to an [`iced_native`] +/// modifiers state. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +pub fn modifiers_state( + modifiers: winit::event::ModifiersState, +) -> ModifiersState { + ModifiersState { + shift: modifiers.shift(), + control: modifiers.ctrl(), + alt: modifiers.alt(), + logo: modifiers.logo(), + } +} + +/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native @@ -220,3 +332,13 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::Cut => KeyCode::Cut, } } + +// As defined in: http://www.unicode.org/faq/private_use.html +pub(crate) fn is_private_use_character(c: char) -> bool { + match c { + '\u{E000}'..='\u{F8FF}' + | '\u{F0000}'..='\u{FFFFD}' + | '\u{100000}'..='\u{10FFFD}' => true, + _ => false, + } +} diff --git a/winit/src/debug/basic.rs b/winit/src/debug/basic.rs index 67c6d8a2..d46edba6 100644 --- a/winit/src/debug/basic.rs +++ b/winit/src/debug/basic.rs @@ -1,5 +1,4 @@ -use std::collections::VecDeque; -use std::time; +use std::{collections::VecDeque, time}; #[derive(Debug)] pub struct Debug { @@ -146,7 +145,7 @@ impl Debug { let mut lines = Vec::new(); fn key_value<T: std::fmt::Debug>(key: &str, value: T) -> String { - format!("{: <30} {:?}", key, value) + format!("{} {:?}", key, value) } lines.push(format!( diff --git a/winit/src/debug/null.rs b/winit/src/debug/null.rs index 9c809dd4..2a9430cd 100644 --- a/winit/src/debug/null.rs +++ b/winit/src/debug/null.rs @@ -6,8 +6,6 @@ impl Debug { Self } - pub fn toggle(&mut self) {} - pub fn startup_started(&mut self) {} pub fn startup_finished(&mut self) {} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 00d200f9..f99e1290 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -18,8 +18,8 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] #[doc(no_inline)] pub use iced_native::*; @@ -29,14 +29,24 @@ pub mod conversion; pub mod settings; mod application; - -pub use application::Application; -pub use settings::Settings; +mod clipboard; +mod mode; +mod proxy; +mod size; // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. -#[cfg_attr(feature = "debug", path = "debug/basic.rs")] -#[cfg_attr(not(feature = "debug"), path = "debug/null.rs")] +#[cfg(feature = "debug")] +#[path = "debug/basic.rs"] +mod debug; +#[cfg(not(feature = "debug"))] +#[path = "debug/null.rs"] mod debug; +pub use application::Application; +pub use clipboard::Clipboard; +pub use mode::Mode; +pub use settings::Settings; + use debug::Debug; +use proxy::Proxy; diff --git a/winit/src/mode.rs b/winit/src/mode.rs new file mode 100644 index 00000000..37464711 --- /dev/null +++ b/winit/src/mode.rs @@ -0,0 +1,9 @@ +/// The mode of a window-based application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// The application appears in its own window. + Windowed, + + /// The application takes the whole screen of its current monitor. + Fullscreen, +} diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs new file mode 100644 index 00000000..cff6ca72 --- /dev/null +++ b/winit/src/proxy.rs @@ -0,0 +1,58 @@ +use iced_native::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, +}; +use std::pin::Pin; + +pub struct Proxy<Message: 'static> { + raw: winit::event_loop::EventLoopProxy<Message>, +} + +impl<Message: 'static> Clone for Proxy<Message> { + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + } + } +} + +impl<Message: 'static> Proxy<Message> { + pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self { + Self { raw } + } +} + +impl<Message: 'static> Sink<Message> for Proxy<Message> { + type Error = mpsc::SendError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn start_send( + self: Pin<&mut Self>, + message: Message, + ) -> Result<(), Self::Error> { + let _ = self.raw.send_event(message); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } +} diff --git a/winit/src/settings/mod.rs b/winit/src/settings/mod.rs new file mode 100644 index 00000000..b2290b46 --- /dev/null +++ b/winit/src/settings/mod.rs @@ -0,0 +1,53 @@ +//! Configure your application. +#[cfg(target_os = "windows")] +#[path = "windows.rs"] +mod platform; +#[cfg(not(target_os = "windows"))] +#[path = "not_windows.rs"] +mod platform; + +pub use platform::PlatformSpecific; + +/// The settings of an application. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The [`Window`] settings + /// + /// [`Window`]: struct.Window.html + pub window: Window, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + window: Window::default(), + } + } +} + +/// The window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Window { + /// The size of the window. + pub size: (u32, u32), + + /// Whether the window should be resizable or not. + pub resizable: bool, + + /// Whether the window should have a border, a title bar, etc. + pub decorations: bool, + + /// Platform specific settings. + pub platform_specific: platform::PlatformSpecific, +} + +impl Default for Window { + fn default() -> Window { + Window { + size: (1024, 768), + resizable: true, + decorations: true, + platform_specific: Default::default(), + } + } +} diff --git a/winit/src/settings/not_windows.rs b/winit/src/settings/not_windows.rs new file mode 100644 index 00000000..5d703f84 --- /dev/null +++ b/winit/src/settings/not_windows.rs @@ -0,0 +1,6 @@ +#![cfg(not(target_os = "windows"))] +//! Platform specific settings for not Windows. + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific {} diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs new file mode 100644 index 00000000..76b8d067 --- /dev/null +++ b/winit/src/settings/windows.rs @@ -0,0 +1,9 @@ +#![cfg(target_os = "windows")] +//! Platform specific settings for Windows. + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific { + /// Parent Window + pub parent: Option<winapi::shared::windef::HWND>, +} diff --git a/winit/src/size.rs b/winit/src/size.rs new file mode 100644 index 00000000..7e3056d4 --- /dev/null +++ b/winit/src/size.rs @@ -0,0 +1,30 @@ +pub struct Size { + physical: winit::dpi::PhysicalSize<u32>, + logical: winit::dpi::LogicalSize<f64>, + scale_factor: f64, +} + +impl Size { + pub fn new( + physical: winit::dpi::PhysicalSize<u32>, + scale_factor: f64, + ) -> Size { + Size { + logical: physical.to_logical(scale_factor), + physical, + scale_factor, + } + } + + pub fn physical(&self) -> winit::dpi::PhysicalSize<u32> { + self.physical + } + + pub fn logical(&self) -> winit::dpi::LogicalSize<f64> { + self.logical + } + + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } +} |