diff options
340 files changed, 12771 insertions, 5658 deletions
@@ -1,4 +1,4 @@ -/target +target/ pkg/ **/*.rs.bk Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 59471abc..ee650337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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 +- `"system_font"` feature gates reading system fonts. [#370] + +[#370]: https://github.com/hecrj/iced/pull/370 ## [0.1.1] - 2020-04-15 ### Added @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.1.1" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A cross-platform GUI library inspired by Elm" @@ -12,16 +12,33 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +default = ["wgpu", "default_system_font"] +# Enables the `iced_wgpu` renderer +wgpu = ["iced_wgpu"] # Enables the `Image` widget image = ["iced_wgpu/image"] # Enables the `Svg` widget svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget canvas = ["iced_wgpu/canvas"] +# Enables the `QRCode` widget +qr_code = ["iced_wgpu/qr_code"] +# Enables using system fonts +default_system_font = ["iced_wgpu/default_system_font"] +# Enables the `iced_glow` renderer. Overrides `iced_wgpu` +glow = ["iced_glow", "iced_glutin"] +# Enables the `Canvas` widget for `iced_glow` +glow_canvas = ["iced_glow/canvas"] +# Enables the `QRCode` widget for `iced_glow` +glow_qr_code = ["iced_glow/qr_code"] +# Enables using system fonts for `iced_glow` +glow_default_system_font = ["iced_glow/default_system_font"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] +# Enables old `tokio` (0.2) as the `executor::Default` on native platforms +tokio_old = ["iced_futures/tokio_old"] # Enables `async-std` as the `executor::Default` on native platforms async-std = ["iced_futures/async-std"] # Enables advanced color conversion via `palette` @@ -34,6 +51,9 @@ maintenance = { status = "actively-developed" } members = [ "core", "futures", + "graphics", + "glow", + "glutin", "native", "style", "web", @@ -50,8 +70,11 @@ members = [ "examples/geometry", "examples/integration", "examples/pane_grid", + "examples/pick_list", "examples/pokedex", "examples/progress_bar", + "examples/qr_code", + "examples/scrollable", "examples/solar_system", "examples/stopwatch", "examples/styling", @@ -61,15 +84,18 @@ members = [ ] [dependencies] -iced_core = { version = "0.2", path = "core" } -iced_futures = { version = "0.1", path = "futures" } +iced_core = { version = "0.3", path = "core" } +iced_futures = { version = "0.2", path = "futures" } +thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_winit = { version = "0.1", path = "winit" } -iced_wgpu = { version = "0.2", path = "wgpu" } +iced_winit = { version = "0.2", path = "winit" } +iced_glutin = { version = "0.1", path = "glutin", optional = true } +iced_wgpu = { version = "0.3", path = "wgpu", optional = true } +iced_glow = { version = "0.1", path = "glow", optional = true} [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_web = { version = "0.2", path = "web" } +iced_web = { version = "0.3", path = "web" } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 65815b96..82303130 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -1,10 +1,7 @@ # Ecosystem -This document describes the Iced ecosystem. - -It quickly lists the different audiences of the library and explains how the different crates relate to each other. - -## Users +This document describes the Iced ecosystem and explains how the different crates relate to each other. +## Overview Iced is meant to be used by 2 different types of users: - __End-users__. They should be able to: @@ -18,71 +15,81 @@ Iced is meant to be used by 2 different types of users: - integrate existing runtimes in their own system (like game engines), - and create their own custom renderers. -## Crates Iced consists of different crates which offer different layers of abstractions for our users. This modular architecture helps us keep implementation details hidden and decoupled, which should allow us to rewrite or change strategies in the future. - +<p align="center"> + <img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="60%"> +</p> -### [`iced_core`] -[`iced_core`] holds basic reusable types of the public API. For instance, basic data types like `Point`, `Rectangle`, `Length`, etc. +## The foundations +There are a bunch of concepts that permeate the whole ecosystem. These concepts are considered __the foundations__, and they are provided by three different crates: -This crate is meant to be a starting point for an Iced runtime. +- [`iced_core`] contains many lightweight, reusable primitives (e.g. `Point`, `Rectangle`, `Color`). +- [`iced_futures`] implements the concurrent concepts of [The Elm Architecture] on top of the [`futures`] ecosystem. +- [`iced_style`] defines the default styling capabilities of built-in widgets. -### [`iced_native`] -[`iced_native`] takes [`iced_core`] and builds a native runtime on top of it, featuring: -- A custom layout engine, greatly inspired by [`druid`] -- Event handling for all the built-in widgets -- A renderer-agnostic API +<p align="center"> + <img alt="The foundations" src="docs/graphs/foundations.png" width="50%"> +</p> -To achieve this, it introduces a bunch of reusable interfaces: -- 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 `Backend` 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. +## The native target +The native side of the ecosystem is split into two different groups: __renderers__ and __shells__. -[`druid`]: https://github.com/xi-editor/druid -[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle +<p align="center"> + <img alt="The native target" src="docs/graphs/native.png" width="80%"> +</p> -### [`iced_web`] -[`iced_web`] takes [`iced_core`] and builds a WebAssembly runtime on top. It achieves this by introducing a `Widget` trait that can be used to produce VDOM nodes. +### Renderers +The widgets of a _graphical_ user interface produce some primitives that eventually need to be drawn on screen. __Renderers__ take care of this task, potentially leveraging GPU acceleration. -The crate is currently a simple abstraction layer over [`dodrio`]. +Currently, there are two different official renderers: -[`dodrio`]: https://github.com/fitzgen/dodrio +- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal. +- [`iced_glow`] is powered by [`glow`] and supports OpenGL 3.3+. -### [`iced_wgpu`] -[`iced_wgpu`] is a [`wgpu`] renderer for [`iced_native`]. For now, it is the default renderer of Iced in native platforms. +Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate. -[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API]. +### Shells +The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop. -Currently, [`iced_wgpu`] supports the following primitives: -- Text, which is rendered using [`wgpu_glyph`]. No shaping at all. -- Quads or rectangles, with rounded borders and a solid background color. -- Clip areas, useful to implement scrollables or hide overflowing content. -- Images and SVG, loaded from memory or the file system. -- Meshes of triangles, useful to draw geometry freely. +Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture]. -[`wgpu`]: https://github.com/gfx-rs/wgpu-rs -[WebGPU API]: https://gpuweb.github.io/gpuweb/ -[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph +As of now, there are two official shells: -### [`iced_winit`] -[`iced_winit`] offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`]. +- [`iced_winit`] implements a shell runtime on top of [`winit`]. +- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation]. -It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop. +## The web target +The Web platform provides all the abstractions necessary to draw widgets and gather users interactions. -[`winit`]: https://github.com/rust-windowing/winit +Therefore, unlike the native path, the web side of the ecosystem does not need to split renderers and shells. Instead, [`iced_web`] leverages [`dodrio`] to both render widgets and implement a proper runtime. -### [`iced`] +## Iced Finally, [`iced`] unifies everything into a simple abstraction to create cross-platform applications: -- On native, it uses [`iced_winit`] and [`iced_wgpu`]. +- On native, it uses __[shells](#shells)__ and __[renderers](#renderers)__. - On the web, it uses [`iced_web`]. -This is the crate meant to be used by __end-users__. +<p align="center"> + <img alt="Iced" src="docs/graphs/iced.png" width="80%"> +</p> [`iced_core`]: core +[`iced_futures`]: futures +[`iced_style`]: style [`iced_native`]: native [`iced_web`]: web +[`iced_graphics`]: graphics [`iced_wgpu`]: wgpu +[`iced_glow`]: glow [`iced_winit`]: winit +[`iced_glutin`]: glutin [`iced`]: .. +[`futures`]: https://github.com/rust-lang/futures-rs +[`glow`]: https://github.com/grovesNL/glow +[`wgpu`]: https://github.com/gfx-rs/wgpu-rs +[`winit`]: https://github.com/rust-windowing/winit +[`glutin`]: https://github.com/rust-windowing/glutin +[`dodrio`]: https://github.com/fitzgen/dodrio +[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context +[The Elm Architecture]: https://guide.elm-lang.org/architecture/ @@ -55,7 +55,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.1" +iced = "0.2" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If @@ -168,26 +168,19 @@ Browse the [documentation] and the [examples] to learn more! Iced was originally born as an attempt at bringing the simplicity of [Elm] and [The Elm Architecture] into [Coffee], a 2D game engine I am working on. -The core of the library was implemented during May in [this pull request]. +The core of the library was implemented during May 2019 in [this pull request]. [The first alpha version] was eventually released as [a renderer-agnostic GUI library]. The library did not provide a renderer and implemented the current [tour example] on top of [`ggez`], a game library. Since then, the focus has shifted towards providing a batteries-included, -end-user-oriented GUI library, while keeping [the ecosystem] modular. +end-user-oriented GUI library, while keeping [the ecosystem] modular: -Currently, Iced is a cross-platform GUI library built on top of smaller crates: - - - [`iced_core`], a bunch of basic types that can be reused in different runtimes. - - [`iced_native`], a renderer-agnostic native runtime implementing widget - logic and a layout engine inspired by [`druid`]. - - [`iced_web`], an experimental web runtime that targets the DOM thanks to - [`dodrio`]. - - [`iced_wgpu`], a renderer leveraging [`wgpu`], [`wgpu_glyph`], and - [`font-kit`]. - - [`iced_winit`], a windowing shell on top of [`winit`]. - -[](https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md) +<p align="center"> + <a href="https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md"> + <img alt="Iced Ecosystem" src="docs/graphs/ecosystem.png" width="80%"> + </a> +</p> [this pull request]: https://github.com/hecrj/coffee/pull/35 [The first alpha version]: https://github.com/hecrj/iced/tree/0.1.0-alpha @@ -195,15 +188,6 @@ Currently, Iced is a cross-platform GUI library built on top of smaller crates: [tour example]: https://github.com/hecrj/iced/blob/master/examples/README.md#tour [`ggez`]: https://github.com/ggez/ggez [the ecosystem]: https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md -[`iced_core`]: https://github.com/hecrj/iced/tree/master/core -[`iced_native`]: https://github.com/hecrj/iced/tree/master/native -[`iced_web`]: https://github.com/hecrj/iced/tree/master/web -[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu -[`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit -[`druid`]: https://github.com/xi-editor/druid -[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph -[`font-kit`]: https://github.com/servo/font-kit -[`winit`]: https://github.com/rust-windowing/winit ## Contributing / Feedback Contributions are greatly appreciated! If you want to contribute, please @@ -6,7 +6,7 @@ Before diving into the roadmap, check out [the ecosystem overview] to get an ide [the ecosystem overview]: ECOSYSTEM.md ## Next steps -Most of the work related to these features needs to happen in the `iced_native` path of the ecosystem, as the web already supports many of them. +Most of the work related to these features needs to happen in the __native__ path of the ecosystem, as the web already supports many of them. Once a step is completed, it is collapsed and added to this list: @@ -17,6 +17,8 @@ Once a step is completed, it is collapsed and added to this list: * [x] Custom layout engine ([#52]) * [x] Event subscriptions ([#122]) * [x] Custom styling ([#146]) + * [x] Canvas for 2D graphics ([#193]) + * [x] Basic overlay support ([#444]) [#24]: https://github.com/hecrj/iced/issues/24 [#25]: https://github.com/hecrj/iced/issues/25 @@ -25,6 +27,8 @@ Once a step is completed, it is collapsed and added to this list: [#52]: https://github.com/hecrj/iced/pull/52 [#122]: https://github.com/hecrj/iced/pull/122 [#146]: https://github.com/hecrj/iced/pull/146 +[#193]: https://github.com/hecrj/iced/pull/193 +[#444]: https://github.com/hecrj/iced/pull/444 ### Multi-window support ([#27]) Open and control multiple windows at runtime. @@ -35,17 +39,6 @@ This approach should also allow us to perform custom optimizations for this part [#27]: https://github.com/hecrj/iced/issues/27 -### Layers ([#30]) -Currently, Iced assumes widgets cannot be laid out on top of each other. We should implement support for multiple layers of widgets. - -This is a necessary feature to implement many kinds of interactables, like dropdown menus, select fields, etc. - -`iced_native` will need to group widgets to perform layouting and process some events first for widgets positioned on top. - -`iced_wgpu` will also need to process the scene graph and sort draw calls based on the different layers. - -[#30]: https://github.com/hecrj/iced/issues/30 - ### Animations ([#31]) Allow widgets to request a redraw at a specific time. @@ -55,8 +48,8 @@ This is a necessary feature to render loading spinners, a blinking text cursor, [#31]: https://github.com/hecrj/iced/issues/31 -### Canvas widget ([#32]) -A widget to draw freely in 2D or 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc. +### Canvas widget for 3D graphics ([#32]) +A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc. As a first approach, we could expose the underlying renderer directly here, and couple this widget with it ([`wgpu`] for now). Once [`wgpu`] gets WebGL or WebGPU support, this widget will be able to run on the web too. The renderer primitive could be a simple texture that the widget draws to. diff --git a/core/Cargo.toml b/core/Cargo.toml index b52bf315..a859c868 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.2.1" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "The essential concepts of Iced" diff --git a/core/README.md b/core/README.md index 641612c0..3ec053ac 100644 --- a/core/README.md +++ b/core/README.md @@ -8,7 +8,9 @@ This crate is meant to be a starting point for an Iced runtime. - +<p align="center"> + <img alt="The foundations" src="../docs/graphs/foundations.png" width="50%"> +</p> [documentation]: https://docs.rs/iced_core @@ -16,7 +18,7 @@ This crate is meant to be a starting point for an Iced runtime. Add `iced_core` as a dependency in your `Cargo.toml`: ```toml -iced_core = "0.2" +iced_core = "0.3" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/core/src/background.rs b/core/src/background.rs index e1a37ddc..cfb95867 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -13,3 +13,9 @@ impl From<Color> for Background { Background::Color(color) } } + +impl From<Color> for Option<Background> { + fn from(color: Color) -> Self { + Some(Background::from(color)) + } +} diff --git a/core/src/color.rs b/core/src/color.rs index a4c3d87c..c66ee97c 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -43,8 +43,6 @@ impl Color { /// /// In debug mode, it will panic if the values are not in the correct /// range: 0.0 - 1.0 - /// - /// [`Color`]: struct.Color.html pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color { debug_assert!( (0.0..=1.0).contains(&r), @@ -67,29 +65,21 @@ impl Color { } /// Creates a [`Color`] from its RGB components. - /// - /// [`Color`]: struct.Color.html pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color { Color::from_rgba(r, g, b, 1.0f32) } /// Creates a [`Color`] from its RGBA components. - /// - /// [`Color`]: struct.Color.html pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color { Color { r, g, b, a } } /// 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, @@ -100,8 +90,6 @@ impl Color { } /// Converts the [`Color`] into its linear values. - /// - /// [`Color`]: struct.Color.html pub fn into_linear(self) -> [f32; 4] { // As described in: // https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation @@ -122,8 +110,6 @@ impl Color { } /// Inverts the [`Color`] in-place. - /// - /// [`Color`]: struct.Color.html pub fn invert(&mut self) { self.r = 1.0f32 - self.r; self.b = 1.0f32 - self.g; @@ -131,8 +117,6 @@ impl Color { } /// Returns the inverted [`Color`]. - /// - /// [`Color`]: struct.Color.html pub fn inverse(self) -> Color { Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) } @@ -152,8 +136,6 @@ impl From<[f32; 4]> for Color { #[cfg(feature = "palette")] /// Converts from palette's `Srgba` type to a [`Color`]. -/// -/// [`Color`]: struct.Color.html impl From<Srgba> for Color { fn from(srgba: Srgba) -> Self { Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha) @@ -162,8 +144,6 @@ impl From<Srgba> for Color { #[cfg(feature = "palette")] /// Converts from [`Color`] to palette's `Srgba` type. -/// -/// [`Color`]: struct.Color.html impl From<Color> for Srgba { fn from(c: Color) -> Self { Srgba::new(c.r, c.g, c.b, c.a) @@ -172,8 +152,6 @@ impl From<Color> for Srgba { #[cfg(feature = "palette")] /// Converts from palette's `Srgb` type to a [`Color`]. -/// -/// [`Color`]: struct.Color.html impl From<Srgb> for Color { fn from(srgb: Srgb) -> Self { Color::new(srgb.red, srgb.green, srgb.blue, 1.0) @@ -182,9 +160,6 @@ impl From<Srgb> for Color { #[cfg(feature = "palette")] /// Converts from [`Color`] to palette's `Srgb` type. -/// -/// [`Color`]: struct.Color.html -/// [`Srgb`]: ../palette/rgb/type.Srgb.html impl From<Color> for Srgb { fn from(c: Color) -> Self { Srgb::new(c.r, c.g, c.b) diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index b26bdb3d..61e017ad 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,8 +1,8 @@ //! Reuse basic keyboard types. mod event; mod key_code; -mod modifiers_state; +mod modifiers; pub use event::Event; pub use key_code::KeyCode; -pub use modifiers_state::ModifiersState; +pub use modifiers::Modifiers; diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index bc8437a8..0564c171 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,4 +1,4 @@ -use super::{KeyCode, ModifiersState}; +use super::{KeyCode, Modifiers}; /// A keyboard event. /// @@ -14,7 +14,7 @@ pub enum Event { key_code: KeyCode, /// The state of the modifier keys - modifiers: ModifiersState, + modifiers: Modifiers, }, /// A keyboard key was released. @@ -23,9 +23,12 @@ pub enum Event { key_code: KeyCode, /// The state of the modifier keys - modifiers: ModifiersState, + modifiers: Modifiers, }, /// A unicode character was received. CharacterReceived(char), + + /// The keyboard modifiers have changed. + ModifiersChanged(Modifiers), } diff --git a/core/src/keyboard/key_code.rs b/core/src/keyboard/key_code.rs index 26020a57..74ead170 100644 --- a/core/src/keyboard/key_code.rs +++ b/core/src/keyboard/key_code.rs @@ -55,7 +55,7 @@ pub enum KeyCode { Y, Z, - /// The Escape key, next to F1 + /// The Escape key, next to F1. Escape, F1, @@ -83,14 +83,14 @@ pub enum KeyCode { F23, F24, - /// Print Screen/SysRq + /// Print Screen/SysRq. Snapshot, - /// Scroll Lock + /// Scroll Lock. Scroll, - /// Pause/Break key, next to Scroll lock + /// Pause/Break key, next to Scroll lock. Pause, - /// `Insert`, next to Backspace + /// `Insert`, next to Backspace. Insert, Home, Delete, @@ -103,11 +103,14 @@ pub enum KeyCode { Right, Down, + /// The Backspace key, right over Enter. Backspace, + /// The Enter key. Enter, + /// The space bar. Space, - /// The "Compose" key on Linux + /// The "Compose" key on Linux. Compose, Caret, @@ -123,12 +126,20 @@ pub enum KeyCode { Numpad7, Numpad8, Numpad9, + NumpadAdd, + NumpadDivide, + NumpadDecimal, + NumpadComma, + NumpadEnter, + NumpadEquals, + NumpadMultiply, + NumpadSubtract, AbntC1, AbntC2, - Add, Apostrophe, Apps, + Asterisk, At, Ax, Backslash, @@ -137,8 +148,6 @@ pub enum KeyCode { Colon, Comma, Convert, - Decimal, - Divide, Equals, Grave, Kana, @@ -152,19 +161,16 @@ pub enum KeyCode { MediaSelect, MediaStop, Minus, - Multiply, Mute, MyComputer, - NavigateForward, // also called "Prior" - NavigateBackward, // also called "Next" + NavigateForward, // also called "Next" + NavigateBackward, // also called "Prior" NextTrack, NoConvert, - NumpadComma, - NumpadEnter, - NumpadEquals, OEM102, Period, PlayPause, + Plus, Power, PrevTrack, RAlt, @@ -176,7 +182,6 @@ pub enum KeyCode { Slash, Sleep, Stop, - Subtract, Sysrq, Tab, Underline, diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs new file mode 100644 index 00000000..d2a0500e --- /dev/null +++ b/core/src/keyboard/modifiers.rs @@ -0,0 +1,45 @@ +/// The current state of the keyboard modifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Modifiers { + /// 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, +} + +impl Modifiers { + /// Returns true if a "command key" is pressed in the [`Modifiers`]. + /// + /// The "command key" is the main modifier key used to issue commands in the + /// current platform. Specifically: + /// + /// - It is the `logo` or command key (⌘) on macOS + /// - It is the `control` key on other platforms + pub fn is_command_pressed(self) -> bool { + #[cfg(target_os = "macos")] + let is_pressed = self.logo; + + #[cfg(not(target_os = "macos"))] + let is_pressed = self.control; + + is_pressed + } + + /// Returns true if the current [`Modifiers`] have at least the same + /// keys pressed as the provided ones, and false otherwise. + pub fn matches(&self, modifiers: Self) -> bool { + let shift = !modifiers.shift || self.shift; + let control = !modifiers.control || self.control; + let alt = !modifiers.alt || self.alt; + let logo = !modifiers.logo || self.logo; + + shift && control && alt && logo + } +} diff --git a/core/src/keyboard/modifiers_state.rs b/core/src/keyboard/modifiers_state.rs deleted file mode 100644 index 4d24266f..00000000 --- a/core/src/keyboard/modifiers_state.rs +++ /dev/null @@ -1,30 +0,0 @@ -/// The current state of the keyboard modifiers. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -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, -} - -impl ModifiersState { - /// Returns true if the current [`ModifiersState`] has at least the same - /// modifiers enabled as the given value, and false otherwise. - /// - /// [`ModifiersState`]: struct.ModifiersState.html - pub fn matches(&self, modifiers: ModifiersState) -> bool { - let shift = !modifiers.shift || self.shift; - let control = !modifiers.control || self.control; - let alt = !modifiers.alt || self.alt; - let logo = !modifiers.logo || self.logo; - - shift && control && alt && logo - } -} diff --git a/core/src/length.rs b/core/src/length.rs index 06d8cf0a..186411a5 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -26,8 +26,6 @@ impl Length { /// The _fill factor_ is a relative unit describing how much of the /// remaining space should be filled when compared to other elements. It /// is only meant to be used by layout engines. - /// - /// [`Length`]: enum.Length.html pub fn fill_factor(&self) -> u16 { match self { Length::Fill => 1, diff --git a/core/src/lib.rs b/core/src/lib.rs index 6b9e612e..f2d21a5f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,11 +1,11 @@ //! The core library of [Iced]. //! -//!  -//! //! This library holds basic types that can be reused and re-exported in //! different runtime implementations. For instance, both [`iced_native`] and //! [`iced_web`] are built on top of `iced_core`. //! +//!  +//! //! [Iced]: https://github.com/hecrj/iced //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native //! [`iced_web`]: https://github.com/hecrj/iced/tree/master/web diff --git a/core/src/mouse/event.rs b/core/src/mouse/event.rs index 2f07b207..321b8399 100644 --- a/core/src/mouse/event.rs +++ b/core/src/mouse/event.rs @@ -1,3 +1,5 @@ +use crate::Point; + use super::Button; /// A mouse event. @@ -16,11 +18,8 @@ pub enum Event { /// The mouse cursor was moved CursorMoved { - /// The X coordinate of the mouse position - x: f32, - - /// The Y coordinate of the mouse position - y: f32, + /// The new position of the mouse cursor + position: Point, }, /// A mouse button was pressed. diff --git a/core/src/point.rs b/core/src/point.rs index 3714aa2f..9bf7726b 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -12,20 +12,14 @@ pub struct Point { impl Point { /// The origin (i.e. a [`Point`] at (0, 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 pub const fn new(x: f32, y: f32) -> Self { Self { x, y } } /// Computes the distance to another [`Point`]. - /// - /// [`Point`]: struct.Point.html pub fn distance(&self, to: Point) -> f32 { let a = self.x - to.x; let b = self.y - to.y; @@ -46,6 +40,12 @@ impl From<[u16; 2]> for Point { } } +impl From<Point> for [f32; 2] { + fn from(point: Point) -> [f32; 2] { + [point.x, point.y] + } +} + impl std::ops::Add<Vector> for Point { type Output = Self; diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 8bc89a44..0a7f5fe2 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -19,10 +19,6 @@ pub struct Rectangle<T = f32> { impl Rectangle<f32> { /// Creates a new [`Rectangle`] with its top-left corner in the given /// [`Point`] and with the provided [`Size`]. - /// - /// [`Rectangle`]: struct.Rectangle.html - /// [`Point`]: struct.Point.html - /// [`Size`]: struct.Size.html pub fn new(top_left: Point, size: Size) -> Self { Self { x: top_left.x, @@ -34,9 +30,6 @@ impl Rectangle<f32> { /// Creates a new [`Rectangle`] with its top-left corner at the origin /// and with the provided [`Size`]. - /// - /// [`Rectangle`]: struct.Rectangle.html - /// [`Size`]: struct.Size.html pub fn with_size(size: Size) -> Self { Self { x: 0.0, @@ -47,50 +40,33 @@ impl Rectangle<f32> { } /// Returns the [`Point`] at the center of the [`Rectangle`]. - /// - /// [`Point`]: struct.Point.html - /// [`Rectangle`]: struct.Rectangle.html pub fn center(&self) -> Point { Point::new(self.center_x(), self.center_y()) } /// Returns the X coordinate of the [`Point`] at the center of the /// [`Rectangle`]. - /// - /// [`Point`]: struct.Point.html - /// [`Rectangle`]: struct.Rectangle.html pub fn center_x(&self) -> f32 { self.x + self.width / 2.0 } /// Returns the Y coordinate of the [`Point`] at the center of the /// [`Rectangle`]. - /// - /// [`Point`]: struct.Point.html - /// [`Rectangle`]: struct.Rectangle.html pub fn center_y(&self) -> f32 { self.y + self.height / 2.0 } /// Returns the position of the top left corner of the [`Rectangle`]. - /// - /// [`Rectangle`]: struct.Rectangle.html pub fn position(&self) -> Point { Point::new(self.x, self.y) } /// Returns the [`Size`] of the [`Rectangle`]. - /// - /// [`Size`]: struct.Size.html - /// [`Rectangle`]: struct.Rectangle.html pub fn size(&self) -> Size { Size::new(self.width, self.height) } /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. - /// - /// [`Point`]: struct.Point.html - /// [`Rectangle`]: struct.Rectangle.html pub fn contains(&self, point: Point) -> bool { self.x <= point.x && point.x <= self.x + self.width @@ -99,8 +75,6 @@ impl Rectangle<f32> { } /// Computes the intersection with the given [`Rectangle`]. - /// - /// [`Rectangle`]: struct.Rectangle.html pub fn intersection( &self, other: &Rectangle<f32>, @@ -125,17 +99,27 @@ impl Rectangle<f32> { None } } + + /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. + pub fn snap(self) -> Rectangle<u32> { + Rectangle { + x: self.x as u32, + y: self.y as u32, + width: self.width.ceil() as u32, + height: self.height.ceil() as u32, + } + } } -impl std::ops::Mul<f32> for Rectangle<u32> { +impl std::ops::Mul<f32> for Rectangle<f32> { type Output = Self; fn mul(self, scale: f32) -> Self { Self { - x: (self.x as f32 * scale).round() as u32, - y: (self.y as f32 * scale).round() as u32, - width: (self.width as f32 * scale).round() as u32, - height: (self.height as f32 * scale).round() as u32, + x: self.x as f32 * scale, + y: self.y as f32 * scale, + width: self.width * scale, + height: self.height * scale, } } } @@ -151,17 +135,6 @@ impl From<Rectangle<u32>> for Rectangle<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 + 0.5).round() as u32, - height: (rectangle.height + 0.5).round() as u32, - } - } -} - impl<T> std::ops::Add<Vector<T>> for Rectangle<T> where T: std::ops::Add<Output = T>, diff --git a/core/src/size.rs b/core/src/size.rs index a02299e8..9ea9e686 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,40 +1,33 @@ +use crate::Vector; use std::f32; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Size { +pub struct Size<T = f32> { /// The width. - pub width: f32, + pub width: T, /// The height. - pub height: f32, + pub height: T, +} + +impl<T> Size<T> { + /// Creates a new [`Size`] with the given width and height. + pub const fn new(width: T, height: T) -> Self { + Size { width, height } + } } impl Size { /// A [`Size`] with zero width and height. - /// - /// [`Size`]: struct.Size.html pub const ZERO: Size = Size::new(0., 0.); /// A [`Size`] with a width and height of 1 unit. - /// - /// [`Size`]: struct.Size.html pub const UNIT: Size = Size::new(1., 1.); /// A [`Size`] with infinite width and height. - /// - /// [`Size`]: struct.Size.html pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); - /// Creates a new [`Size`] with the given width and height. - /// - /// [`Size`]: struct.Size.html - pub const fn new(width: f32, height: f32) -> Self { - Size { width, height } - } - /// Increments the [`Size`] to account for the given padding. - /// - /// [`Size`]: struct.Size.html pub fn pad(&self, padding: f32) -> Self { Size { width: self.width + padding * 2.0, @@ -54,3 +47,24 @@ impl From<[u16; 2]> for Size { Size::new(width.into(), height.into()) } } + +impl From<Vector<f32>> for Size { + fn from(vector: Vector<f32>) -> Self { + Size { + width: vector.x, + height: vector.y, + } + } +} + +impl From<Size> for [f32; 2] { + fn from(size: Size) -> [f32; 2] { + [size.width, size.height] + } +} + +impl From<Size> for Vector<f32> { + fn from(size: Size) -> Self { + Vector::new(size.width, size.height) + } +} diff --git a/core/src/vector.rs b/core/src/vector.rs index def3f8c0..92bb7648 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -2,20 +2,14 @@ #[derive(Debug, Clone, Copy, PartialEq)] pub struct Vector<T = f32> { /// The X component of the [`Vector`] - /// - /// [`Vector`]: struct.Vector.html pub x: T, /// The Y component of the [`Vector`] - /// - /// [`Vector`]: struct.Vector.html pub y: T, } impl<T> Vector<T> { /// Creates a new [`Vector`] with the given components. - /// - /// [`Vector`]: struct.Vector.html pub const fn new(x: T, y: T) -> Self { Self { x, y } } @@ -65,3 +59,18 @@ where } } } + +impl<T> From<[T; 2]> for Vector<T> { + fn from([x, y]: [T; 2]) -> Self { + Self::new(x, y) + } +} + +impl<T> From<Vector<T>> for [T; 2] +where + T: Copy, +{ + fn from(other: Vector<T>) -> Self { + [other.x, other.y] + } +} diff --git a/docs/graphs/core.dot b/docs/graphs/core.dot deleted file mode 100644 index 93724927..00000000 --- a/docs/graphs/core.dot +++ /dev/null @@ -1,13 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - { rank = same; iced_native iced_web } - - iced_core -> iced_native [style=dashed]; - iced_core -> iced_web [style=dashed]; - - iced_core [style=dashed]; -} diff --git a/docs/graphs/core.png b/docs/graphs/core.png Binary files differdeleted file mode 100644 index 0b14ab6c..00000000 --- a/docs/graphs/core.png +++ /dev/null diff --git a/docs/graphs/ecosystem.dot b/docs/graphs/ecosystem.dot deleted file mode 100644 index 609cf726..00000000 --- a/docs/graphs/ecosystem.dot +++ /dev/null @@ -1,56 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - subgraph cluster_1 { - label = "renderers "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_1 [label="...", style=solid, shape=none]; - iced_wgpu; - } - - subgraph cluster_2 { - label = "shells "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_2 [label="...", style=solid, shape=none]; - iced_winit; - } - - subgraph cluster_3 { - style=invis; - margin=20; - iced; - } - - { rank = same; iced_native iced_web } - { rank = same; iced_wgpu iced_winit etc_1 etc_2 } - - iced_core -> iced_native [style=dashed]; - iced_core -> iced_web [style=dashed]; - iced_native -> iced_wgpu; - iced_native -> iced_winit; - - iced_winit -> iced; - iced_wgpu -> iced; - iced_web -> iced; - - iced -> "cross-platform application"; - - iced_core [style=dashed]; - - "cross-platform application" [shape=box, width=2.8, height=0.6]; -} diff --git a/docs/graphs/ecosystem.png b/docs/graphs/ecosystem.png Binary files differindex 03fe1130..8b418c52 100644 --- a/docs/graphs/ecosystem.png +++ b/docs/graphs/ecosystem.png diff --git a/docs/graphs/foundations.png b/docs/graphs/foundations.png Binary files differnew file mode 100644 index 00000000..cc043c99 --- /dev/null +++ b/docs/graphs/foundations.png diff --git a/docs/graphs/generate.sh b/docs/graphs/generate.sh deleted file mode 100755 index 45073820..00000000 --- a/docs/graphs/generate.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -for file in *.dot -do - dot -Tpng ${file} -o ${file%.*}.png -done diff --git a/docs/graphs/iced.dot b/docs/graphs/iced.dot deleted file mode 100644 index 24dbb972..00000000 --- a/docs/graphs/iced.dot +++ /dev/null @@ -1,46 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - subgraph cluster_1 { - label = "renderers "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_1 [label="...", style=solid, shape=none]; - iced_wgpu; - } - - subgraph cluster_2 { - label = "shells "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_2 [label="...", style=solid, shape=none]; - iced_winit; - } - - subgraph cluster_3 { - style=invis; - margin=20; - iced; - } - - { rank = same; iced_wgpu iced_winit etc_1 etc_2 } - - iced_winit -> iced; - iced_wgpu -> iced; - iced_web -> iced; - - iced; -} diff --git a/docs/graphs/iced.png b/docs/graphs/iced.png Binary files differindex 5d4a35bc..bf777e53 100644 --- a/docs/graphs/iced.png +++ b/docs/graphs/iced.png diff --git a/docs/graphs/native.dot b/docs/graphs/native.dot deleted file mode 100644 index b57736b5..00000000 --- a/docs/graphs/native.dot +++ /dev/null @@ -1,41 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - subgraph cluster_1 { - label = "renderers "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_1 [label="...", style=solid, shape=none]; - iced_wgpu; - } - - subgraph cluster_2 { - label = "shells "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_2 [label="...", style=solid, shape=none]; - iced_winit; - } - - - { rank = same; iced_wgpu iced_winit etc_1 etc_2 } - - iced_core -> iced_native [style=dashed]; - iced_native -> iced_wgpu; - iced_native -> iced_winit; - - iced_core [style=dashed]; -} diff --git a/docs/graphs/native.png b/docs/graphs/native.png Binary files differindex 892e4fee..6a8759e0 100644 --- a/docs/graphs/native.png +++ b/docs/graphs/native.png diff --git a/docs/graphs/web.dot b/docs/graphs/web.dot deleted file mode 100644 index 853ca398..00000000 --- a/docs/graphs/web.dot +++ /dev/null @@ -1,12 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - iced_core -> iced_web [style=dashed]; - - iced_web -> iced; - - iced_core [style=dashed]; -} diff --git a/docs/graphs/web.png b/docs/graphs/web.png Binary files differdeleted file mode 100644 index e6a1a5f6..00000000 --- a/docs/graphs/web.png +++ /dev/null diff --git a/docs/graphs/wgpu.dot b/docs/graphs/wgpu.dot deleted file mode 100644 index 410c2eeb..00000000 --- a/docs/graphs/wgpu.dot +++ /dev/null @@ -1,31 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - subgraph cluster_1 { - label = "renderers "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_1 [label="...", style=solid, shape=none]; - iced_wgpu; - } - - subgraph cluster_3 { - style=invis; - margin=20; - iced; - } - - { rank = same; iced_wgpu etc_1 } - - iced_native -> iced_wgpu; - - iced_wgpu -> iced; -} diff --git a/docs/graphs/wgpu.png b/docs/graphs/wgpu.png Binary files differdeleted file mode 100644 index 4831caba..00000000 --- a/docs/graphs/wgpu.png +++ /dev/null diff --git a/docs/graphs/winit.dot b/docs/graphs/winit.dot deleted file mode 100644 index 4ea5149a..00000000 --- a/docs/graphs/winit.dot +++ /dev/null @@ -1,31 +0,0 @@ -digraph G { - fontname = "Roboto"; - newrank=true; - node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"]; - edge[color="#333333"]; - - subgraph cluster_2 { - label = "shells "; - labelloc = "b"; - labeljust = "r"; - fontcolor = "#0366d6"; - color="#f6f8fa"; - bgcolor="#f6f8fa"; - style=rounded; - - etc_2 [label="...", style=solid, shape=none]; - iced_winit; - } - - subgraph cluster_3 { - style=invis; - margin=20; - iced; - } - - { rank = same; iced_winit etc_2 } - - iced_native -> iced_winit; - - iced_winit -> iced; -} diff --git a/docs/graphs/winit.png b/docs/graphs/winit.png Binary files differdeleted file mode 100644 index 1c028b29..00000000 --- a/docs/graphs/winit.png +++ /dev/null diff --git a/examples/README.md b/examples/README.md index 8e1b781f..10c28cf5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -100,8 +100,10 @@ A bunch of simpler examples exist: - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). - [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application. - [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized. +- [`pick_list`](pick_list), a dropdown list of selectable options. - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. +- [`scrollable`](scrollable), a showcase of the various scrollbar width options. - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. - [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time. - [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. @@ -116,7 +118,7 @@ cargo run --package <example> [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 +Since [Iced was born in May 2019], it has been powering the user interfaces in [Coffee], an experimental 2D game engine. @@ -126,6 +128,6 @@ Since [Iced was born in May], it has been powering the user interfaces in </a> </div> -[Iced was born in May]: https://github.com/hecrj/coffee/pull/35 +[Iced was born in May 2019]: https://github.com/hecrj/coffee/pull/35 [`ui` module]: https://docs.rs/coffee/0.3.2/coffee/ui/index.html [Coffee]: https://github.com/hecrj/coffee diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fe41e1b2..97832e01 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -3,11 +3,11 @@ use iced::{ button, Align, Button, Column, Element, Length, Sandbox, Settings, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings { antialiasing: true, ..Settings::default() - }); + }) } #[derive(Default)] @@ -69,7 +69,8 @@ impl Sandbox for Example { mod bezier { use iced::{ - canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, + canvas::event::{self, Event}, + canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke}, mouse, Element, Length, Point, Rectangle, }; @@ -109,40 +110,51 @@ mod bezier { event: Event, bounds: Rectangle, cursor: Cursor, - ) -> Option<Curve> { - let cursor_position = cursor.position_in(&bounds)?; + ) -> (event::Status, Option<Curve>) { + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - match self.state.pending { - None => { - self.state.pending = Some(Pending::One { - from: cursor_position, - }); - None - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - Some(Curve { - from, - to, - control: cursor_position, - }) + Event::Mouse(mouse_event) => { + let message = match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: cursor_position, + }); + + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: cursor_position, + }) + } } } - } - _ => None, - }, + _ => None, + }; + + (event::Status::Captured, message) + } + _ => (event::Status::Ignored, None), } } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 9c583c78..b317ac00 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -4,7 +4,7 @@ use iced::{ Point, Rectangle, Settings, Subscription, Vector, }; -pub fn main() { +pub fn main() -> iced::Result { Clock::run(Settings { antialiasing: true, ..Settings::default() diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index cec6ac79..bb2c61cb 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -8,7 +8,7 @@ use palette::{self, Hsl, Limited, Srgb}; use std::marker::PhantomData; use std::ops::RangeInclusive; -pub fn main() { +pub fn main() -> iced::Result { ColorPalette::run(Settings { antialiasing: true, ..Settings::default() @@ -269,7 +269,7 @@ struct ColorPicker<C: ColorSpace> { trait ColorSpace: Sized { const LABEL: &'static str; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3]; + const COMPONENT_RANGES: [RangeInclusive<f64>; 3]; fn new(a: f32, b: f32, c: f32) -> Self; @@ -284,13 +284,25 @@ impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { let [s1, s2, s3] = &mut self.sliders; let [cr1, cr2, cr3] = C::COMPONENT_RANGES; + fn slider<C: Clone>( + state: &mut slider::State, + range: RangeInclusive<f64>, + component: f32, + update: impl Fn(f32) -> C + 'static, + ) -> Slider<f64, C> { + Slider::new(state, range, f64::from(component), move |v| { + update(v as f32) + }) + .step(0.01) + } + Row::new() .spacing(10) .align_items(Align::Center) .push(Text::new(C::LABEL).width(Length::Units(50))) - .push(Slider::new(s1, cr1, c1, move |v| C::new(v, c2, c3))) - .push(Slider::new(s2, cr2, c2, move |v| C::new(c1, v, c3))) - .push(Slider::new(s3, cr3, c3, move |v| C::new(c1, c2, v))) + .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3))) + .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3))) + .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v))) .push( Text::new(color.to_string()) .width(Length::Units(185)) @@ -302,7 +314,7 @@ impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { impl ColorSpace for Color { const LABEL: &'static str = "RGB"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; fn new(r: f32, g: f32, b: f32) -> Self { @@ -325,7 +337,7 @@ impl ColorSpace for Color { impl ColorSpace for palette::Hsl { const LABEL: &'static str = "HSL"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, saturation: f32, lightness: f32) -> Self { @@ -356,7 +368,7 @@ impl ColorSpace for palette::Hsl { impl ColorSpace for palette::Hsv { const LABEL: &'static str = "HSV"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, saturation: f32, value: f32) -> Self { @@ -379,7 +391,7 @@ impl ColorSpace for palette::Hsv { impl ColorSpace for palette::Hwb { const LABEL: &'static str = "HWB"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { @@ -410,7 +422,7 @@ impl ColorSpace for palette::Hwb { impl ColorSpace for palette::Lab { const LABEL: &'static str = "Lab"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; fn new(l: f32, a: f32, b: f32) -> Self { @@ -428,7 +440,7 @@ impl ColorSpace for palette::Lab { impl ColorSpace for palette::Lch { const LABEL: &'static str = "Lch"; - const COMPONENT_RANGES: [RangeInclusive<f32>; 3] = + const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; fn new(l: f32, chroma: f32, hue: f32) -> Self { diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index bde0ea94..e0b2ebd6 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,6 +1,6 @@ use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text}; -pub fn main() { +pub fn main() -> iced::Result { Counter::run(Settings::default()) } diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml index 30747dc0..3942538d 100644 --- a/examples/custom_widget/Cargo.toml +++ b/examples/custom_widget/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index f096fb54..36f468c7 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -9,23 +9,26 @@ mod circle { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::{Backend, Defaults, Primitive, Renderer}; use iced_native::{ layout, mouse, Background, Color, Element, Hasher, Layout, Length, - Point, Size, Widget, + Point, Rectangle, Size, Widget, }; - use iced_wgpu::{Defaults, Primitive, Renderer}; pub struct Circle { - radius: u16, + radius: f32, } impl Circle { - pub fn new(radius: u16) -> Self { + pub fn new(radius: f32) -> Self { Self { radius } } } - impl<Message> Widget<Message, Renderer> for Circle { + impl<Message, B> Widget<Message, Renderer<B>> for Circle + where + B: Backend, + { fn width(&self) -> Length { Length::Shrink } @@ -36,34 +39,32 @@ mod circle { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, _limits: &layout::Limits, ) -> layout::Node { - layout::Node::new(Size::new( - f32::from(self.radius) * 2.0, - f32::from(self.radius) * 2.0, - )) + layout::Node::new(Size::new(self.radius * 2.0, self.radius * 2.0)) } fn hash_layout(&self, state: &mut Hasher) { use std::hash::Hash; - self.radius.hash(state); + self.radius.to_bits().hash(state); } fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> (Primitive, mouse::Interaction) { ( Primitive::Quad { bounds: layout.bounds(), background: Background::Color(Color::BLACK), border_radius: self.radius, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, mouse::Interaction::default(), @@ -71,8 +72,11 @@ mod circle { } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Circle + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } @@ -84,12 +88,12 @@ use iced::{ Slider, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } struct Example { - radius: u16, + radius: f32, slider: slider::State, } @@ -103,7 +107,7 @@ impl Sandbox for Example { fn new() -> Self { Example { - radius: 50, + radius: 50.0, slider: slider::State::new(), } } @@ -115,7 +119,7 @@ impl Sandbox for Example { fn update(&mut self, message: Message) { match message { Message::RadiusChanged(radius) => { - self.radius = radius.round() as u16; + self.radius = radius; } } } @@ -127,13 +131,16 @@ impl Sandbox for Example { .max_width(500) .align_items(Align::Center) .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {}", self.radius.to_string()))) - .push(Slider::new( - &mut self.slider, - 1.0..=100.0, - f32::from(self.radius), - Message::RadiusChanged, - )); + .push(Text::new(format!("Radius: {:.2}", self.radius))) + .push( + Slider::new( + &mut self.slider, + 1.0..=100.0, + self.radius, + Message::RadiusChanged, + ) + .step(0.01), + ); Container::new(content) .width(Length::Fill) diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml index 34e6a132..4b05e7dc 100644 --- a/examples/download_progress/Cargo.toml +++ b/examples/download_progress/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["tokio"] } +iced = { path = "../..", features = ["tokio_old"] } iced_native = { path = "../../native" } iced_futures = { path = "../../futures" } reqwest = "0.10" diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index c37ae678..77b01354 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -5,7 +5,7 @@ use iced::{ mod download; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 066fc230..6eba6aad 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -3,7 +3,7 @@ use iced::{ Element, Length, Settings, Subscription, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Events::run(Settings::default()) } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index b9bb7f2a..9c4172c4 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "tokio", "debug"] } -tokio = { version = "0.2", features = ["blocking"] } +tokio = { version = "0.3", features = ["sync"] } itertools = "0.9" rustc-hash = "1.1" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md index 1aeb1455..aa39201c 100644 --- a/examples/game_of_life/README.md +++ b/examples/game_of_life/README.md @@ -7,8 +7,8 @@ It runs a simulation in a background thread while allowing interaction with a `C The __[`main`]__ file contains the relevant code of the example. <div align="center"> - <a href="https://gfycat.com/briefaccurateaardvark"> - <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + <a href="https://gfycat.com/WhichPaltryChick"> + <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif"> </a> </div> diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 080d55c0..e18bd6e0 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,18 +1,22 @@ //! This example showcases an interactive version of the Game of Life, invented //! by John Conway. It leverages a `Canvas` together with other widgets. +mod preset; mod style; use grid::Grid; +use iced::button::{self, Button}; +use iced::executor; +use iced::pick_list::{self, PickList}; +use iced::slider::{self, Slider}; +use iced::time; use iced::{ - button::{self, Button}, - executor, - slider::{self, Slider}, - time, Align, Application, Checkbox, Column, Command, Container, Element, - Length, Row, Settings, Subscription, Text, + Align, Application, Checkbox, Column, Command, Container, Element, Length, + Row, Settings, Subscription, Text, }; +use preset::Preset; use std::time::{Duration, Instant}; -pub fn main() { +pub fn main() -> iced::Result { GameOfLife::run(Settings { antialiasing: true, ..Settings::default() @@ -27,17 +31,19 @@ struct GameOfLife { queued_ticks: usize, speed: usize, next_speed: Option<usize>, + version: usize, } #[derive(Debug, Clone)] enum Message { - Grid(grid::Message), + Grid(grid::Message, usize), Tick(Instant), TogglePlayback, ToggleGrid(bool), Next, Clear, SpeedChanged(f32), + PresetPicked(Preset), } impl Application for GameOfLife { @@ -48,7 +54,7 @@ impl Application for GameOfLife { fn new(_flags: ()) -> (Self, Command<Message>) { ( Self { - speed: 1, + speed: 5, ..Self::default() }, Command::none(), @@ -61,8 +67,10 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::Grid(message) => { - self.grid.update(message); + Message::Grid(message, version) => { + if version == self.version { + self.grid.update(message); + } } Message::Tick(_) | Message::Next => { self.queued_ticks = (self.queued_ticks + 1).min(self.speed); @@ -74,7 +82,11 @@ impl Application for GameOfLife { self.queued_ticks = 0; - return Command::perform(task, Message::Grid); + let version = self.version; + + return Command::perform(task, move |message| { + Message::Grid(message, version) + }); } } Message::TogglePlayback => { @@ -85,6 +97,7 @@ impl Application for GameOfLife { } Message::Clear => { self.grid.clear(); + self.version += 1; } Message::SpeedChanged(speed) => { if self.is_playing { @@ -93,6 +106,10 @@ impl Application for GameOfLife { self.speed = speed.round() as usize; } } + Message::PresetPicked(new_preset) => { + self.grid = Grid::from_preset(new_preset); + self.version += 1; + } } Command::none() @@ -108,15 +125,21 @@ impl Application for GameOfLife { } fn view(&mut self) -> Element<Message> { + let version = self.version; let selected_speed = self.next_speed.unwrap_or(self.speed); let controls = self.controls.view( self.is_playing, self.grid.are_lines_visible(), selected_speed, + self.grid.preset(), ); let content = Column::new() - .push(self.grid.view().map(Message::Grid)) + .push( + self.grid + .view() + .map(move |message| Message::Grid(message, version)), + ) .push(controls); Container::new(content) @@ -128,10 +151,10 @@ impl Application for GameOfLife { } mod grid { + use crate::Preset; use iced::{ - canvas::{ - self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, - }, + canvas::event::{self, Event}, + canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text}, mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, Size, Vector, VerticalAlignment, }; @@ -142,6 +165,7 @@ mod grid { pub struct Grid { state: State, + preset: Preset, interaction: Interaction, life_cache: Cache, grid_cache: Cache, @@ -150,7 +174,6 @@ mod grid { show_lines: bool, last_tick_duration: Duration, last_queued_ticks: usize, - version: usize, } #[derive(Debug, Clone)] @@ -160,7 +183,6 @@ mod grid { Ticked { result: Result<Life, TickError>, tick_duration: Duration, - version: usize, }, } @@ -171,8 +193,24 @@ mod grid { impl Default for Grid { fn default() -> Self { + Self::from_preset(Preset::default()) + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn from_preset(preset: Preset) -> Self { Self { - state: State::default(), + state: State::with_life( + preset + .life() + .into_iter() + .map(|(i, j)| Cell { i, j }) + .collect(), + ), + preset, interaction: Interaction::None, life_cache: Cache::default(), grid_cache: Cache::default(), @@ -181,20 +219,13 @@ mod grid { show_lines: true, last_tick_duration: Duration::default(), last_queued_ticks: 0, - version: 0, } } - } - - impl Grid { - const MIN_SCALING: f32 = 0.1; - const MAX_SCALING: f32 = 2.0; pub fn tick( &mut self, amount: usize, ) -> Option<impl Future<Output = Message>> { - let version = self.version; let tick = self.state.tick(amount)?; self.last_queued_ticks = amount; @@ -206,7 +237,6 @@ mod grid { Message::Ticked { result, - version, tick_duration, } }) @@ -217,16 +247,19 @@ mod grid { Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); + + self.preset = Preset::Custom; } Message::Unpopulate(cell) => { self.state.unpopulate(&cell); self.life_cache.clear(); + + self.preset = Preset::Custom; } Message::Ticked { result: Ok(life), - version, tick_duration, - } if version == self.version => { + } => { self.state.update(life); self.life_cache.clear(); @@ -237,7 +270,6 @@ mod grid { } => { dbg!(error); } - Message::Ticked { .. } => {} } } @@ -250,11 +282,15 @@ mod grid { pub fn clear(&mut self) { self.state = State::default(); - self.version += 1; + self.preset = Preset::Custom; self.life_cache.clear(); } + pub fn preset(&self) -> Preset { + self.preset + } + pub fn toggle_lines(&mut self, enabled: bool) { self.show_lines = enabled; } @@ -291,12 +327,18 @@ mod grid { event: Event, bounds: Rectangle, cursor: Cursor, - ) -> Option<Message> { + ) -> (event::Status, Option<Message>) { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { self.interaction = Interaction::None; } - let cursor_position = cursor.position_in(&bounds)?; + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; + let cell = Cell::at(self.project(cursor_position, bounds.size())); let is_populated = self.state.contains(&cell); @@ -308,28 +350,32 @@ mod grid { match event { Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(button) => match button { - mouse::Button::Left => { - self.interaction = if is_populated { - Interaction::Erasing - } else { - Interaction::Drawing - }; - - populate.or(unpopulate) - } - mouse::Button::Right => { - self.interaction = Interaction::Panning { - translation: self.translation, - start: cursor_position, - }; + mouse::Event::ButtonPressed(button) => { + let message = match button { + mouse::Button::Left => { + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + self.interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; - None - } - _ => None, - }, + None + } + _ => None, + }; + + (event::Status::Captured, message) + } mouse::Event::CursorMoved { .. } => { - match self.interaction { + let message = match self.interaction { Interaction::Drawing => populate, Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { @@ -343,7 +389,14 @@ mod grid { None } _ => None, - } + }; + + let event_status = match self.interaction { + Interaction::None => event::Status::Ignored, + _ => event::Status::Captured, + }; + + (event_status, message) } mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } @@ -376,11 +429,12 @@ mod grid { self.grid_cache.clear(); } - None + (event::Status::Captured, None) } }, - _ => None, + _ => (event::Status::Ignored, None), }, + _ => (event::Status::Ignored, None), } } @@ -533,6 +587,13 @@ mod grid { } impl State { + pub fn with_life(life: Life) -> Self { + Self { + life, + ..Self::default() + } + } + fn cell_count(&self) -> usize { self.life.len() + self.births.len() } @@ -647,6 +708,14 @@ mod grid { } } + impl std::iter::FromIterator<Cell> for Life { + fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self { + Life { + cells: iter.into_iter().collect(), + } + } + } + impl std::fmt::Debug for Life { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Life") @@ -741,6 +810,7 @@ struct Controls { next_button: button::State, clear_button: button::State, speed_slider: slider::State, + preset_list: pick_list::State<Preset>, } impl Controls { @@ -749,6 +819,7 @@ impl Controls { is_playing: bool, is_grid_enabled: bool, speed: usize, + preset: Preset, ) -> Element<'a, Message> { let playback_controls = Row::new() .spacing(10) @@ -794,6 +865,17 @@ impl Controls { .text_size(16), ) .push( + PickList::new( + &mut self.preset_list, + preset::ALL, + Some(preset), + Message::PresetPicked, + ) + .padding(8) + .text_size(16) + .style(style::PickList), + ) + .push( Button::new(&mut self.clear_button, Text::new("Clear")) .on_press(Message::Clear) .style(style::Clear), diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs new file mode 100644 index 00000000..05157b6a --- /dev/null +++ b/examples/game_of_life/src/preset.rs @@ -0,0 +1,142 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Preset { + Custom, + XKCD, + Glider, + SmallExploder, + Exploder, + TenCellRow, + LightweightSpaceship, + Tumbler, + GliderGun, + Acorn, +} + +pub static ALL: &[Preset] = &[ + Preset::Custom, + Preset::XKCD, + Preset::Glider, + Preset::SmallExploder, + Preset::Exploder, + Preset::TenCellRow, + Preset::LightweightSpaceship, + Preset::Tumbler, + Preset::GliderGun, + Preset::Acorn, +]; + +impl Preset { + pub fn life(self) -> Vec<(isize, isize)> { + #[rustfmt::skip] + let cells = match self { + Preset::Custom => vec![], + Preset::XKCD => vec![ + " xxx ", + " x x ", + " x x ", + " x ", + "x xxx ", + " x x x ", + " x x", + " x x ", + " x x ", + ], + Preset::Glider => vec![ + " x ", + " x", + "xxx" + ], + Preset::SmallExploder => vec![ + " x ", + "xxx", + "x x", + " x ", + ], + Preset::Exploder => vec![ + "x x x", + "x x", + "x x", + "x x", + "x x x", + ], + Preset::TenCellRow => vec![ + "xxxxxxxxxx", + ], + Preset::LightweightSpaceship => vec![ + " xxxxx", + "x x", + " x", + "x x ", + ], + Preset::Tumbler => vec![ + " xx xx ", + " xx xx ", + " x x ", + "x x x x", + "x x x x", + "xx xx", + ], + Preset::GliderGun => vec![ + " x ", + " x x ", + " xx xx xx", + " x x xx xx", + "xx x x xx ", + "xx x x xx x x ", + " x x x ", + " x x ", + " xx ", + ], + Preset::Acorn => vec![ + " x ", + " x ", + "xx xxx", + ], + }; + + let start_row = -(cells.len() as isize / 2); + + cells + .into_iter() + .enumerate() + .flat_map(|(i, cells)| { + let start_column = -(cells.len() as isize / 2); + + cells + .chars() + .enumerate() + .filter(|(_, c)| !c.is_whitespace()) + .map(move |(j, _)| { + (start_row + i as isize, start_column + j as isize) + }) + }) + .collect() + } +} + +impl Default for Preset { + fn default() -> Preset { + Preset::XKCD + } +} + +impl std::fmt::Display for Preset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Preset::Custom => "Custom", + Preset::XKCD => "xkcd #2293", + Preset::Glider => "Glider", + Preset::SmallExploder => "Small Exploder", + Preset::Exploder => "Exploder", + Preset::TenCellRow => "10 Cell Row", + Preset::LightweightSpaceship => "Lightweight spaceship", + Preset::Tumbler => "Tumbler", + Preset::GliderGun => "Gosper Glider Gun", + Preset::Acorn => "Acorn", + } + ) + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs index d59569f2..6605826f 100644 --- a/examples/game_of_life/src/style.rs +++ b/examples/game_of_life/src/style.rs @@ -1,4 +1,4 @@ -use iced::{button, container, slider, Background, Color}; +use iced::{button, container, pick_list, slider, Background, Color}; const ACTIVE: Color = Color::from_rgb( 0x72 as f32 / 255.0, @@ -18,6 +18,12 @@ const HOVERED: Color = Color::from_rgb( 0xC4 as f32 / 255.0, ); +const BACKGROUND: Color = Color::from_rgb( + 0x2F as f32 / 255.0, + 0x31 as f32 / 255.0, + 0x36 as f32 / 255.0, +); + pub struct Container; impl container::StyleSheet for Container { @@ -38,7 +44,7 @@ impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { background: Some(Background::Color(ACTIVE)), - border_radius: 3, + border_radius: 3.0, text_color: Color::WHITE, ..button::Style::default() } @@ -54,7 +60,7 @@ impl button::StyleSheet for Button { fn pressed(&self) -> button::Style { button::Style { - border_width: 1, + border_width: 1.0, border_color: Color::WHITE, ..self.hovered() } @@ -67,7 +73,7 @@ impl button::StyleSheet for Clear { fn active(&self) -> button::Style { button::Style { background: Some(Background::Color(DESTRUCTIVE)), - border_radius: 3, + border_radius: 3.0, text_color: Color::WHITE, ..button::Style::default() } @@ -86,7 +92,7 @@ impl button::StyleSheet for Clear { fn pressed(&self) -> button::Style { button::Style { - border_width: 1, + border_width: 1.0, border_color: Color::WHITE, ..self.hovered() } @@ -100,9 +106,9 @@ impl slider::StyleSheet for Slider { slider::Style { rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9 }, + shape: slider::HandleShape::Circle { radius: 9.0 }, color: ACTIVE, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } @@ -132,3 +138,51 @@ impl slider::StyleSheet for Slider { } } } + +pub struct PickList; + +impl pick_list::StyleSheet for PickList { + fn menu(&self) -> pick_list::Menu { + pick_list::Menu { + text_color: Color::WHITE, + background: BACKGROUND.into(), + border_width: 1.0, + border_color: Color { + a: 0.7, + ..Color::BLACK + }, + selected_background: Color { + a: 0.5, + ..Color::BLACK + } + .into(), + selected_text_color: Color::WHITE, + } + } + + fn active(&self) -> pick_list::Style { + pick_list::Style { + text_color: Color::WHITE, + background: BACKGROUND.into(), + border_width: 1.0, + border_color: Color { + a: 0.6, + ..Color::BLACK + }, + border_radius: 2.0, + icon_size: 0.5, + } + } + + fn hovered(&self) -> pick_list::Style { + let active = self.active(); + + pick_list::Style { + border_color: Color { + a: 0.9, + ..Color::BLACK + }, + ..active + } + } +} diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 9df52454..34eec4fb 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced = { path = "../.." } iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } +iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index aabe6b21..f650b2c1 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -10,13 +10,13 @@ mod rainbow { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, - Widget, - }; - use iced_wgpu::{ + use iced_graphics::{ triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, + Backend, Defaults, Primitive, Renderer, + }; + use iced_native::{ + layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size, + Vector, Widget, }; pub struct Rainbow; @@ -27,7 +27,10 @@ mod rainbow { } } - impl<Message> Widget<Message, Renderer> for Rainbow { + impl<Message, B> Widget<Message, Renderer<B>> for Rainbow + where + B: Backend, + { fn width(&self) -> Length { Length::Fill } @@ -38,7 +41,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer<B>, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -50,10 +53,11 @@ mod rainbow { fn draw( &self, - _renderer: &mut Renderer, + _renderer: &mut Renderer<B>, _defaults: &Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> (Primitive, mouse::Interaction) { let b = layout.bounds(); @@ -146,8 +150,11 @@ mod rainbow { } } - impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow { - fn into(self) -> Element<'a, Message, Renderer> { + impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow + where + B: Backend, + { + fn into(self) -> Element<'a, Message, Renderer<B>> { Element::new(self) } } @@ -159,7 +166,7 @@ use iced::{ }; use rainbow::Rainbow; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index afc2c791..4515502f 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,4 +8,4 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu" } -env_logger = "0.7" +env_logger = "0.8" diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 0999336b..824f9f53 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,15 +1,15 @@ -use crate::Scene; - use iced_wgpu::Renderer; use iced_winit::{ - slider, Align, Color, Column, Element, Length, Row, Slider, Text, + slider, Align, Color, Column, Command, Element, Length, Program, Row, + Slider, Text, }; pub struct Controls { + background_color: Color, sliders: [slider::State; 3], } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), } @@ -17,58 +17,64 @@ pub enum Message { impl Controls { pub fn new() -> Controls { Controls { + background_color: Color::BLACK, sliders: Default::default(), } } - pub fn update(&self, message: Message, scene: &mut Scene) { + pub fn background_color(&self) -> Color { + self.background_color + } +} + +impl Program for Controls { + type Renderer = Renderer; + type Message = Message; + + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::BackgroundColorChanged(color) => { - scene.background_color = color; + self.background_color = color; } } + + Command::none() } - pub fn view(&mut self, scene: &Scene) -> Element<Message, Renderer> { + fn view(&mut self) -> Element<Message, Renderer> { let [r, g, b] = &mut self.sliders; - let background_color = scene.background_color; + let background_color = self.background_color; let sliders = Row::new() .width(Length::Units(500)) .spacing(20) - .push(Slider::new( - r, - 0.0..=1.0, - scene.background_color.r, - move |r| { + .push( + Slider::new(r, 0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color }) - }, - )) - .push(Slider::new( - g, - 0.0..=1.0, - scene.background_color.g, - move |g| { + }) + .step(0.01), + ) + .push( + Slider::new(g, 0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color }) - }, - )) - .push(Slider::new( - b, - 0.0..=1.0, - scene.background_color.b, - move |b| { + }) + .step(0.01), + ) + .push( + Slider::new(b, 0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color }) - }, - )); + }) + .step(0.01), + ); Row::new() .width(Length::Fill) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 92d2fa8d..9b52f3a5 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,14 +4,12 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::{ - wgpu, window::SwapChain, Primitive, Renderer, Settings, Target, -}; -use iced_winit::{ - futures, mouse, winit, Cache, Clipboard, Size, UserInterface, -}; +use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; +use iced_winit::{conversion, futures, program, winit, Debug, Size}; +use futures::task::SpawnExt; use winit::{ + dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, event_loop::{ControlFlow, EventLoop}, }; @@ -22,32 +20,39 @@ pub fn main() { // Initialize winit let event_loop = EventLoop::new(); let window = winit::window::Window::new(&event_loop).unwrap(); - let mut logical_size = - window.inner_size().to_logical(window.scale_factor()); + + let physical_size = window.inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); let mut modifiers = ModifiersState::default(); - // Initialize WGPU + // Initialize wgpu + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + let surface = unsafe { instance.create_surface(&window) }; - let surface = wgpu::Surface::create(&window); let (mut device, queue) = futures::executor::block_on(async { - let adapter = wgpu::Adapter::request( - &wgpu::RequestAdapterOptions { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::Default, compatible_surface: Some(&surface), - }, - wgpu::BackendBit::PRIMARY, - ) - .await - .expect("Request adapter"); + }) + .await + .expect("Request adapter"); adapter - .request_device(&wgpu::DeviceDescriptor { - extensions: wgpu::Extensions { - anisotropic_filtering: false, + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: false, }, - limits: wgpu::Limits::default(), - }) + None, + ) .await + .expect("Request device") }); let format = wgpu::TextureFormat::Bgra8UnormSrgb; @@ -55,20 +60,39 @@ pub fn main() { let mut swap_chain = { let size = window.inner_size(); - SwapChain::new(&device, &surface, format, size.width, size.height) + device.create_swap_chain( + &surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, + ) }; let mut resized = false; - // Initialize iced - let mut events = Vec::new(); - let mut cache = Some(Cache::default()); - let mut renderer = Renderer::new(&mut device, Settings::default()); - let mut output = (Primitive::None, mouse::Interaction::default()); - let clipboard = Clipboard::new(&window); + // Initialize staging belt and local pool + let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); + let mut local_pool = futures::executor::LocalPool::new(); // Initialize scene and GUI controls - let mut scene = Scene::new(&device); - let mut controls = Controls::new(); + let scene = Scene::new(&mut device); + let controls = Controls::new(); + + // Initialize iced + let mut debug = Debug::new(); + let mut renderer = + Renderer::new(Backend::new(&mut device, Settings::default())); + + let mut state = program::State::new( + controls, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); // Run event loop event_loop.run(move |event, _, control_flow| { @@ -78,18 +102,23 @@ pub fn main() { match event { Event::WindowEvent { event, .. } => { match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } WindowEvent::ModifiersChanged(new_modifiers) => { modifiers = new_modifiers; } WindowEvent::Resized(new_size) => { - logical_size = - new_size.to_logical(window.scale_factor()); + viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor(), + ); + resized = true; } WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } - _ => {} } @@ -99,117 +128,95 @@ pub fn main() { window.scale_factor(), modifiers, ) { - events.push(event); + state.queue_event(event); } } Event::MainEventsCleared => { - // If no relevant events happened, we can simply skip this - if events.is_empty() { - return; - } - - // We need to: - // 1. Process events of our user interface. - // 2. Update state as a result of any interaction. - // 3. Generate a new output for our renderer. - - // First, we build our user interface. - let mut user_interface = UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), - &mut renderer, - ); - - // Then, we process the events, obtaining messages in return. - let messages = user_interface.update( - events.drain(..), - clipboard.as_ref().map(|c| c as _), - &renderer, - ); - - let user_interface = if messages.is_empty() { - // If there are no messages, no interactions we care about have - // happened. We can simply leave our user interface as it is. - user_interface - } else { - // If there are messages, we need to update our state - // accordingly and rebuild our user interface. - // We can only do this if we drop our user interface first - // by turning it into its cache. - cache = Some(user_interface.into_cache()); - - // In this example, `Controls` is the only part that cares - // about messages, so updating our state is pretty - // straightforward. - for message in messages { - controls.update(message, &mut scene); - } - - // Once the state has been changed, we rebuild our updated - // user interface. - UserInterface::build( - controls.view(&scene), - Size::new(logical_size.width, logical_size.height), - cache.take().unwrap(), + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + None, &mut renderer, - ) - }; - - // Finally, we just need to draw a new output for our renderer, - output = user_interface.draw(&mut renderer); - - // update our cache, - cache = Some(user_interface.into_cache()); + &mut debug, + ); - // and request a redraw - window.request_redraw(); + // and request a redraw + window.request_redraw(); + } } Event::RedrawRequested(_) => { if resized { let size = window.inner_size(); - swap_chain = SwapChain::new( - &device, + swap_chain = device.create_swap_chain( &surface, - format, - size.width, - size.height, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }, ); + + resized = false; } - let (frame, viewport) = - swap_chain.next_frame().expect("Next frame"); + let frame = swap_chain.get_current_frame().expect("Next frame"); let mut encoder = device.create_command_encoder( &wgpu::CommandEncoderDescriptor { label: None }, ); - // We draw the scene first - scene.draw(&mut encoder, &frame.view); + let program = state.program(); + + { + // We clear the frame + let mut render_pass = scene.clear( + &frame.output.view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } // And then iced on top - let mouse_interaction = renderer.draw( + let mouse_interaction = renderer.backend_mut().draw( &mut device, + &mut staging_belt, &mut encoder, - Target { - texture: &frame.view, - viewport, - }, - &output, - window.scale_factor(), - &["Some debug information!"], + &frame.output.view, + &viewport, + state.primitive(), + &debug.overlay(), ); // Then we submit the work - queue.submit(&[encoder.finish()]); + staging_belt.finish(); + queue.submit(Some(encoder.finish())); - // And update the mouse cursor + // Update the mouse cursor window.set_cursor_icon( iced_winit::conversion::mouse_interaction( mouse_interaction, ), ); + + // And recall staging buffers + local_pool + .spawner() + .spawn(staging_belt.recall()) + .expect("Recall staging buffers"); + + local_pool.run_until_stalled(); } _ => {} } diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 22c6812a..03a338c6 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -2,91 +2,68 @@ use iced_wgpu::wgpu; use iced_winit::Color; pub struct Scene { - pub background_color: Color, pipeline: wgpu::RenderPipeline, - bind_group: wgpu::BindGroup, } impl Scene { pub fn new(device: &wgpu::Device) -> Scene { - let (pipeline, bind_group) = build_pipeline(device); + let pipeline = build_pipeline(device); - Scene { - background_color: Color::BLACK, - pipeline, - bind_group, - } + Scene { pipeline } } - pub fn draw( + pub fn clear<'a>( &self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - ) { - let mut rpass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: { - let [r, g, b, a] = - self.background_color.into_linear(); + target: &'a wgpu::TextureView, + encoder: &'a mut wgpu::CommandEncoder, + background_color: Color, + ) -> wgpu::RenderPass<'a> { + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear({ + let [r, g, b, a] = background_color.into_linear(); - wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, - } - }, - }, - ], - depth_stencil_attachment: None, - }); + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } + }), + store: true, + }, + }], + depth_stencil_attachment: None, + }) + } - rpass.set_pipeline(&self.pipeline); - rpass.set_bind_group(0, &self.bind_group, &[]); - rpass.draw(0..3, 0..1); + pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.draw(0..3, 0..1); } } -fn build_pipeline( - device: &wgpu::Device, -) -> (wgpu::RenderPipeline, wgpu::BindGroup) { - let vs = include_bytes!("shader/vert.spv"); - let fs = include_bytes!("shader/frag.spv"); - - let vs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&vs[..])).unwrap(), - ); +fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { + let vs_module = + device.create_shader_module(wgpu::include_spirv!("shader/vert.spv")); - 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 { - label: None, - bindings: &[], - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &bind_group_layout, - bindings: &[], - }); + let fs_module = + device.create_shader_module(wgpu::include_spirv!("shader/frag.spv")); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&bind_group_layout], + label: None, + push_constant_ranges: &[], + bind_group_layouts: &[], }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &pipeline_layout, + label: None, + layout: Some(&pipeline_layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -98,9 +75,7 @@ fn build_pipeline( rasterization_state: Some(wgpu::RasterizationStateDescriptor { front_face: wgpu::FrontFace::Ccw, cull_mode: wgpu::CullMode::None, - depth_bias: 0, - depth_bias_slope_scale: 0.0, - depth_bias_clamp: 0.0, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -119,5 +94,5 @@ fn build_pipeline( alpha_to_coverage_enabled: false, }); - (pipeline, bind_group) + pipeline } diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml index 3ed912ac..e489f210 100644 --- a/examples/pane_grid/Cargo.toml +++ b/examples/pane_grid/Cargo.toml @@ -6,4 +6,5 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } +iced_native = { path = "../../native" } diff --git a/examples/pane_grid/README.md b/examples/pane_grid/README.md index 3653fc5b..a4cfcb7d 100644 --- a/examples/pane_grid/README.md +++ b/examples/pane_grid/README.md @@ -15,8 +15,8 @@ This example showcases the `PaneGrid` widget, which features: The __[`main`]__ file contains all the code of the example. <div align="center"> - <a href="https://gfycat.com/mixedflatjellyfish"> - <img src="https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif"> + <a href="https://gfycat.com/frailfreshairedaleterrier"> + <img src="https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif"> </a> </div> diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index b4bbd68f..3c3256cf 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,16 +1,18 @@ use iced::{ - button, keyboard, pane_grid, scrollable, Align, Button, Column, Container, - Element, HorizontalAlignment, Length, PaneGrid, Sandbox, Scrollable, - Settings, Text, + button, executor, keyboard, pane_grid, scrollable, Align, Application, + Button, Column, Command, Container, Element, HorizontalAlignment, Length, + PaneGrid, Scrollable, Settings, Subscription, Text, }; +use iced_native::{event, subscription, Event}; -pub fn main() { +pub fn main() -> iced::Result { Example::run(Settings::default()) } struct Example { panes: pane_grid::State<Content>, panes_created: usize, + focus: Option<pane_grid::Pane>, } #[derive(Debug, Clone, Copy)] @@ -18,59 +20,77 @@ enum Message { Split(pane_grid::Axis, pane_grid::Pane), SplitFocused(pane_grid::Axis), FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), Dragged(pane_grid::DragEvent), Resized(pane_grid::ResizeEvent), Close(pane_grid::Pane), CloseFocused, } -impl Sandbox for Example { +impl Application for Example { type Message = Message; + type Executor = executor::Default; + type Flags = (); - fn new() -> Self { + fn new(_flags: ()) -> (Self, Command<Message>) { let (panes, _) = pane_grid::State::new(Content::new(0)); - Example { - panes, - panes_created: 1, - } + ( + Example { + panes, + panes_created: 1, + focus: None, + }, + Command::none(), + ) } fn title(&self) -> String { String::from("Pane grid - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Split(axis, pane) => { - let _ = self.panes.split( + let result = self.panes.split( axis, &pane, Content::new(self.panes_created), ); + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + self.panes_created += 1; } Message::SplitFocused(axis) => { - if let Some(pane) = self.panes.active() { - let _ = self.panes.split( + if let Some(pane) = self.focus { + let result = self.panes.split( axis, &pane, Content::new(self.panes_created), ); + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + self.panes_created += 1; } } Message::FocusAdjacent(direction) => { - if let Some(pane) = self.panes.active() { + if let Some(pane) = self.focus { if let Some(adjacent) = self.panes.adjacent(&pane, direction) { - self.panes.focus(&adjacent); + self.focus = Some(adjacent); } } } + Message::Clicked(pane) => { + self.focus = Some(pane); + } Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { self.panes.resize(&split, ratio); } @@ -82,29 +102,60 @@ impl Sandbox for Example { } Message::Dragged(_) => {} Message::Close(pane) => { - let _ = self.panes.close(&pane); + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } } Message::CloseFocused => { - if let Some(pane) = self.panes.active() { - let _ = self.panes.close(&pane); + if let Some(pane) = self.focus { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } } } } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.is_command_pressed() => handle_hotkey(key_code), + _ => None, + } + }) } fn view(&mut self) -> Element<Message> { + let focus = self.focus; let total_panes = self.panes.len(); - let pane_grid = - PaneGrid::new(&mut self.panes, |pane, content, focus| { - content.view(pane, focus, total_panes) - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_drag(Message::Dragged) - .on_resize(Message::Resized) - .on_key_press(handle_hotkey); + let pane_grid = PaneGrid::new(&mut self.panes, |pane, content| { + let is_focused = focus == Some(pane); + + let title_bar = + pane_grid::TitleBar::new(format!("Pane {}", content.id)) + .padding(10) + .style(style::TitleBar { is_focused }); + + pane_grid::Content::new(content.view(pane, total_panes)) + .title_bar(title_bar) + .style(style::Pane { is_focused }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); Container::new(pane_grid) .width(Length::Fill) @@ -114,11 +165,11 @@ impl Sandbox for Example { } } -fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> { +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { use keyboard::KeyCode; use pane_grid::{Axis, Direction}; - let direction = match event.key_code { + let direction = match key_code { KeyCode::Up => Some(Direction::Up), KeyCode::Down => Some(Direction::Down), KeyCode::Left => Some(Direction::Left), @@ -126,7 +177,7 @@ fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> { _ => None, }; - match event.key_code { + match key_code { KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), KeyCode::W => Some(Message::CloseFocused), @@ -155,15 +206,14 @@ impl Content { fn view( &mut self, pane: pane_grid::Pane, - focus: Option<pane_grid::Focus>, total_panes: usize, ) -> Element<Message> { let Content { - id, scroll, split_horizontally, split_vertically, close, + .. } = self; let button = |state, label, message, style| { @@ -209,7 +259,6 @@ impl Content { .width(Length::Fill) .spacing(10) .align_items(Align::Center) - .push(Text::new(format!("Pane {}", id)).size(30)) .push(controls); Container::new(content) @@ -217,9 +266,6 @@ impl Content { .height(Length::Fill) .padding(5) .center_y() - .style(style::Pane { - is_focused: focus.is_some(), - }) .into() } } @@ -245,6 +291,25 @@ mod style { 0xC4 as f32 / 255.0, ); + pub struct TitleBar { + pub is_focused: bool, + } + + impl container::StyleSheet for TitleBar { + fn style(&self) -> container::Style { + let pane = Pane { + is_focused: self.is_focused, + } + .style(); + + container::Style { + text_color: Some(Color::WHITE), + background: Some(pane.border_color.into()), + ..Default::default() + } + } + } + pub struct Pane { pub is_focused: bool, } @@ -253,10 +318,11 @@ mod style { fn style(&self) -> container::Style { container::Style { background: Some(Background::Color(SURFACE)), - border_width: 2, - border_color: Color { - a: if self.is_focused { 1.0 } else { 0.3 }, - ..Color::BLACK + border_width: 2.0, + border_color: if self.is_focused { + Color::BLACK + } else { + Color::from_rgb(0.7, 0.7, 0.7) }, ..Default::default() } @@ -280,7 +346,7 @@ mod style { button::Style { text_color, background: background.map(Background::Color), - border_radius: 5, + border_radius: 5.0, shadow_offset: Vector::new(0.0, 0.0), ..button::Style::default() } diff --git a/examples/pick_list/Cargo.toml b/examples/pick_list/Cargo.toml new file mode 100644 index 00000000..a87d7217 --- /dev/null +++ b/examples/pick_list/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pick_list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug"] } diff --git a/examples/pick_list/README.md b/examples/pick_list/README.md new file mode 100644 index 00000000..6dc80bf4 --- /dev/null +++ b/examples/pick_list/README.md @@ -0,0 +1,18 @@ +## Pick-list + +A dropdown list of selectable options. + +It displays and positions an overlay based on the window position of the widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif"> +</div> + +You can run it with `cargo run`: +``` +cargo run --package pick_list +``` + +[`main`]: src/main.rs diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs new file mode 100644 index 00000000..68662602 --- /dev/null +++ b/examples/pick_list/src/main.rs @@ -0,0 +1,113 @@ +use iced::{ + pick_list, scrollable, Align, Container, Element, Length, PickList, + Sandbox, Scrollable, Settings, Space, Text, +}; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + scroll: scrollable::State, + pick_list: pick_list::State<Language>, + selected_language: Language, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + LanguageSelected(Language), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("Pick list - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::LanguageSelected(language) => { + self.selected_language = language; + } + } + } + + fn view(&mut self) -> Element<Message> { + let pick_list = PickList::new( + &mut self.pick_list, + &Language::ALL[..], + Some(self.selected_language), + Message::LanguageSelected, + ); + + let mut content = Scrollable::new(&mut self.scroll) + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push(Space::with_height(Length::Units(600))) + .push(Text::new("Which is your favorite language?")) + .push(pick_list); + + content = content.push(Space::with_height(Length::Units(600))); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Javascript, + Other, +} + +impl Language { + const ALL: [Language; 7] = [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Javascript, + Language::Other, + ]; +} + +impl Default for Language { + fn default() -> Language { + Language::Rust + } +} + +impl std::fmt::Display for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Javascript => "Javascript", + Language::Other => "Some other language", + } + ) + } +} diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 94320086..05e73992 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["image", "debug", "tokio"] } +iced = { path = "../..", features = ["image", "debug", "tokio_old"] } serde_json = "1.0" [dependencies.serde] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index e7afa8f5..187e5dee 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -3,7 +3,7 @@ use iced::{ Container, Element, Image, Length, Row, Settings, Text, }; -pub fn main() { +pub fn main() -> iced::Result { Pokedex::run(Settings::default()) } @@ -251,7 +251,7 @@ mod style { background: Some(Background::Color(match self { Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), })), - border_radius: 12, + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::WHITE, ..button::Style::default() diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index 43b09928..c9a8e798 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -1,6 +1,6 @@ use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; -pub fn main() { +pub fn main() -> iced::Result { Progress::run(Settings::default()) } @@ -36,12 +36,15 @@ impl Sandbox for Progress { Column::new() .padding(20) .push(ProgressBar::new(0.0..=100.0, self.value)) - .push(Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - )) + .push( + Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + ) + .step(0.01), + ) .into() } } diff --git a/examples/qr_code/Cargo.toml b/examples/qr_code/Cargo.toml new file mode 100644 index 00000000..7f2d4e42 --- /dev/null +++ b/examples/qr_code/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "qr_code" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["qr_code"] } diff --git a/examples/qr_code/README.md b/examples/qr_code/README.md new file mode 100644 index 00000000..2dd89c26 --- /dev/null +++ b/examples/qr_code/README.md @@ -0,0 +1,18 @@ +## QR Code Generator + +A basic QR code generator that showcases the `QRCode` widget. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://gfycat.com/heavyexhaustedaracari"> + <img src="https://thumbs.gfycat.com/HeavyExhaustedAracari-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package qr_code +``` + +[`main`]: src/main.rs diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs new file mode 100644 index 00000000..37b4855d --- /dev/null +++ b/examples/qr_code/src/main.rs @@ -0,0 +1,81 @@ +use iced::qr_code::{self, QRCode}; +use iced::text_input::{self, TextInput}; +use iced::{ + Align, Column, Container, Element, Length, Sandbox, Settings, Text, +}; + +pub fn main() -> iced::Result { + QRGenerator::run(Settings::default()) +} + +#[derive(Default)] +struct QRGenerator { + data: String, + input: text_input::State, + qr_code: Option<qr_code::State>, +} + +#[derive(Debug, Clone)] +enum Message { + DataChanged(String), +} + +impl Sandbox for QRGenerator { + type Message = Message; + + fn new() -> Self { + QRGenerator { + qr_code: qr_code::State::new("").ok(), + ..Self::default() + } + } + + fn title(&self) -> String { + String::from("QR Code Generator - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::DataChanged(mut data) => { + data.truncate(100); + + self.qr_code = qr_code::State::new(&data).ok(); + self.data = data; + } + } + } + + fn view(&mut self) -> Element<Message> { + let title = Text::new("QR Code Generator") + .size(70) + .color([0.5, 0.5, 0.5]); + + let input = TextInput::new( + &mut self.input, + "Type the data of your QR code here...", + &self.data, + Message::DataChanged, + ) + .size(30) + .padding(15); + + let mut content = Column::new() + .width(Length::Units(700)) + .spacing(20) + .align_items(Align::Center) + .push(title) + .push(input); + + if let Some(qr_code) = self.qr_code.as_mut() { + content = content.push(QRCode::new(qr_code).cell_size(10)); + } + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .padding(20) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml new file mode 100644 index 00000000..12753fb6 --- /dev/null +++ b/examples/scrollable/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "scrollable" +version = "0.1.0" +authors = ["Clark Moody <clark@clarkmoody.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/scrollable/README.md b/examples/scrollable/README.md new file mode 100644 index 00000000..ed0e31b5 --- /dev/null +++ b/examples/scrollable/README.md @@ -0,0 +1,15 @@ +# Scrollable +An example showcasing the various size and style options for the Scrollable. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +<div align="center"> + <a href="./screenshot.png"> + <img src="./screenshot.png" height="640px"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package scrollable +``` diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png Binary files differnew file mode 100644 index 00000000..2d800251 --- /dev/null +++ b/examples/scrollable/screenshot.png diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs new file mode 100644 index 00000000..8dd2e20c --- /dev/null +++ b/examples/scrollable/src/main.rs @@ -0,0 +1,184 @@ +mod style; + +use iced::{ + scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox, + Scrollable, Settings, Space, Text, +}; + +pub fn main() -> iced::Result { + ScrollableDemo::run(Settings::default()) +} + +struct ScrollableDemo { + theme: style::Theme, + variants: Vec<Variant>, +} + +#[derive(Debug, Clone)] +enum Message { + ThemeChanged(style::Theme), +} + +impl Sandbox for ScrollableDemo { + type Message = Message; + + fn new() -> Self { + ScrollableDemo { + theme: Default::default(), + variants: Variant::all(), + } + } + + fn title(&self) -> String { + String::from("Scrollable - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::ThemeChanged(theme) => self.theme = theme, + } + } + + fn view(&mut self) -> Element<Message> { + let ScrollableDemo { + theme, variants, .. + } = self; + + let choose_theme = style::Theme::ALL.iter().fold( + Column::new().spacing(10).push(Text::new("Choose a theme:")), + |column, option| { + column.push( + Radio::new( + *option, + &format!("{:?}", option), + Some(*theme), + Message::ThemeChanged, + ) + .style(*theme), + ) + }, + ); + + let scrollable_row = Row::with_children( + variants + .iter_mut() + .map(|variant| { + let mut scrollable = Scrollable::new(&mut variant.state) + .padding(10) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme) + .push(Text::new(variant.title)); + + if let Some(scrollbar_width) = variant.scrollbar_width { + scrollable = scrollable + .scrollbar_width(scrollbar_width) + .push(Text::new(format!( + "scrollbar_width: {:?}", + scrollbar_width + ))); + } + + if let Some(scrollbar_margin) = variant.scrollbar_margin { + scrollable = scrollable + .scrollbar_margin(scrollbar_margin) + .push(Text::new(format!( + "scrollbar_margin: {:?}", + scrollbar_margin + ))); + } + + if let Some(scroller_width) = variant.scroller_width { + scrollable = scrollable + .scroller_width(scroller_width) + .push(Text::new(format!( + "scroller_width: {:?}", + scroller_width + ))); + } + + scrollable = scrollable + .push(Space::with_height(Length::Units(100))) + .push(Text::new( + "Some content that should wrap within the \ + scrollable. Let's output a lot of short words, so \ + that we'll make sure to see how wrapping works \ + with these scrollbars.", + )) + .push(Space::with_height(Length::Units(1200))) + .push(Text::new("Middle")) + .push(Space::with_height(Length::Units(1200))) + .push(Text::new("The End.")); + + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme) + .into() + }) + .collect(), + ) + .spacing(20) + .width(Length::Fill) + .height(Length::Fill); + + let content = Column::new() + .spacing(20) + .padding(20) + .push(choose_theme) + .push(Rule::horizontal(20).style(self.theme)) + .push(scrollable_row); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .style(self.theme) + .into() + } +} + +/// A version of a scrollable +struct Variant { + title: &'static str, + state: scrollable::State, + scrollbar_width: Option<u16>, + scrollbar_margin: Option<u16>, + scroller_width: Option<u16>, +} + +impl Variant { + pub fn all() -> Vec<Self> { + vec![ + Self { + title: "Default Scrollbar", + state: scrollable::State::new(), + scrollbar_width: None, + scrollbar_margin: None, + scroller_width: None, + }, + Self { + title: "Slimmed & Margin", + state: scrollable::State::new(), + scrollbar_width: Some(4), + scrollbar_margin: Some(3), + scroller_width: Some(4), + }, + Self { + title: "Wide Scroller", + state: scrollable::State::new(), + scrollbar_width: Some(4), + scrollbar_margin: None, + scroller_width: Some(10), + }, + Self { + title: "Narrow Scroller", + state: scrollable::State::new(), + scrollbar_width: Some(10), + scrollbar_margin: None, + scroller_width: Some(4), + }, + ] + } +} diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs new file mode 100644 index 00000000..ae449141 --- /dev/null +++ b/examples/scrollable/src/style.rs @@ -0,0 +1,190 @@ +use iced::{container, radio, rule, scrollable}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +impl Theme { + pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; +} + +impl Default for Theme { + fn default() -> Theme { + Theme::Light + } +} + +impl From<Theme> for Box<dyn container::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Container.into(), + } + } +} + +impl From<Theme> for Box<dyn radio::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Radio.into(), + } + } +} + +impl From<Theme> for Box<dyn scrollable::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Scrollable.into(), + } + } +} + +impl From<Theme> for Box<dyn rule::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Rule.into(), + } + } +} + +mod dark { + use iced::{container, radio, rule, scrollable, Color}; + + const BACKGROUND: Color = Color::from_rgb( + 0x36 as f32 / 255.0, + 0x39 as f32 / 255.0, + 0x3F as f32 / 255.0, + ); + + const SURFACE: Color = Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ); + + const ACCENT: Color = Color::from_rgb( + 0x6F as f32 / 255.0, + 0xFF as f32 / 255.0, + 0xE9 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const SCROLLBAR: Color = Color::from_rgb( + 0x2E as f32 / 255.0, + 0x33 as f32 / 255.0, + 0x38 as f32 / 255.0, + ); + + const SCROLLER: Color = Color::from_rgb( + 0x20 as f32 / 255.0, + 0x22 as f32 / 255.0, + 0x25 as f32 / 255.0, + ); + + pub struct Container; + + impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Color { + a: 0.99, + ..BACKGROUND + } + .into(), + text_color: Color::WHITE.into(), + ..container::Style::default() + } + } + } + + pub struct Radio; + + impl radio::StyleSheet for Radio { + fn active(&self) -> radio::Style { + radio::Style { + background: SURFACE.into(), + dot_color: ACTIVE, + border_width: 1.0, + border_color: ACTIVE, + } + } + + fn hovered(&self) -> radio::Style { + radio::Style { + background: Color { a: 0.5, ..SURFACE }.into(), + ..self.active() + } + } + } + + pub struct Scrollable; + + impl scrollable::StyleSheet for Scrollable { + fn active(&self) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Color { + a: 0.8, + ..SCROLLBAR + } + .into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: Color { a: 0.7, ..SCROLLER }, + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> scrollable::Scrollbar { + let active = self.active(); + + scrollable::Scrollbar { + background: SCROLLBAR.into(), + scroller: scrollable::Scroller { + color: SCROLLER, + ..active.scroller + }, + ..active + } + } + + fn dragging(&self) -> scrollable::Scrollbar { + let hovered = self.hovered(); + + scrollable::Scrollbar { + scroller: scrollable::Scroller { + color: ACCENT, + ..hovered.scroller + }, + ..hovered + } + } + } + + pub struct Rule; + + impl rule::StyleSheet for Rule { + fn style(&self) -> rule::Style { + rule::Style { + color: SURFACE, + width: 2, + radius: 1.0, + fill_mode: rule::FillMode::Percent(30.0), + } + } + } +} diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 98bd3b21..6a2de736 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -14,7 +14,7 @@ use iced::{ use std::time::Instant; -pub fn main() { +pub fn main() -> iced::Result { SolarSystem::run(Settings { antialiasing: true, ..Settings::default() diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 9de6d39e..983cf3e6 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -5,7 +5,7 @@ use iced::{ }; use std::time::{Duration, Instant}; -pub fn main() { +pub fn main() -> iced::Result { Stopwatch::run(Settings::default()) } @@ -161,7 +161,7 @@ mod style { Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), })), - border_radius: 12, + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::WHITE, ..button::Style::default() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 63ab9d62..8975fd9a 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,10 +1,10 @@ use iced::{ button, scrollable, slider, text_input, Align, Button, Checkbox, Column, - Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, - Settings, Slider, Space, Text, TextInput, + Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox, + Scrollable, Settings, Slider, Space, Text, TextInput, }; -pub fn main() { +pub fn main() -> iced::Result { Styling::run(Settings::default()) } @@ -113,14 +113,17 @@ impl Sandbox for Styling { .padding(20) .max_width(600) .push(choose_theme) + .push(Rule::horizontal(38).style(self.theme)) .push(Row::new().spacing(10).push(text_input).push(button)) .push(slider) .push(progress_bar) .push( Row::new() .spacing(10) + .height(Length::Units(100)) .align_items(Align::Center) .push(scrollable) + .push(Rule::vertical(38).style(self.theme)) .push(checkbox), ); @@ -136,8 +139,8 @@ impl Sandbox for Styling { mod style { use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, slider, - text_input, + button, checkbox, container, progress_bar, radio, rule, scrollable, + slider, text_input, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -228,18 +231,25 @@ mod style { } } + impl From<Theme> for Box<dyn rule::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Rule.into(), + } + } + } + mod light { - use iced::{button, Background, Color, Vector}; + use iced::{button, Color, Vector}; pub struct Button; impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.11, 0.42, 0.87, - ))), - border_radius: 12, + background: Color::from_rgb(0.11, 0.42, 0.87).into(), + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), ..button::Style::default() @@ -258,8 +268,8 @@ mod style { mod dark { use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, - slider, text_input, Background, Color, + button, checkbox, container, progress_bar, radio, rule, scrollable, + slider, text_input, Color, }; const SURFACE: Color = Color::from_rgb( @@ -291,10 +301,8 @@ mod style { impl container::StyleSheet for Container { fn style(&self) -> container::Style { container::Style { - background: Some(Background::Color(Color::from_rgb8( - 0x36, 0x39, 0x3F, - ))), - text_color: Some(Color::WHITE), + background: Color::from_rgb8(0x36, 0x39, 0x3F).into(), + text_color: Color::WHITE.into(), ..container::Style::default() } } @@ -305,16 +313,16 @@ mod style { impl radio::StyleSheet for Radio { fn active(&self) -> radio::Style { radio::Style { - background: Background::Color(SURFACE), + background: SURFACE.into(), dot_color: ACTIVE, - border_width: 1, + border_width: 1.0, border_color: ACTIVE, } } fn hovered(&self) -> radio::Style { radio::Style { - background: Background::Color(Color { a: 0.5, ..SURFACE }), + background: Color { a: 0.5, ..SURFACE }.into(), ..self.active() } } @@ -325,16 +333,16 @@ mod style { impl text_input::StyleSheet for TextInput { fn active(&self) -> text_input::Style { text_input::Style { - background: Background::Color(SURFACE), - border_radius: 2, - border_width: 0, + background: SURFACE.into(), + border_radius: 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, } } fn focused(&self) -> text_input::Style { text_input::Style { - border_width: 1, + border_width: 1.0, border_color: ACCENT, ..self.active() } @@ -342,7 +350,7 @@ mod style { fn hovered(&self) -> text_input::Style { text_input::Style { - border_width: 1, + border_width: 1.0, border_color: Color { a: 0.3, ..ACCENT }, ..self.focused() } @@ -366,8 +374,8 @@ mod style { impl button::StyleSheet for Button { fn active(&self) -> button::Style { button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3, + background: ACTIVE.into(), + border_radius: 3.0, text_color: Color::WHITE, ..button::Style::default() } @@ -375,7 +383,7 @@ mod style { fn hovered(&self) -> button::Style { button::Style { - background: Some(Background::Color(HOVERED)), + background: HOVERED.into(), text_color: Color::WHITE, ..self.active() } @@ -383,7 +391,7 @@ mod style { fn pressed(&self) -> button::Style { button::Style { - border_width: 1, + border_width: 1.0, border_color: Color::WHITE, ..self.hovered() } @@ -395,14 +403,14 @@ mod style { impl scrollable::StyleSheet for Scrollable { fn active(&self) -> scrollable::Scrollbar { scrollable::Scrollbar { - background: Some(Background::Color(SURFACE)), - border_radius: 2, - border_width: 0, + background: SURFACE.into(), + border_radius: 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: ACTIVE, - border_radius: 2, - border_width: 0, + border_radius: 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } @@ -412,10 +420,7 @@ mod style { let active = self.active(); scrollable::Scrollbar { - background: Some(Background::Color(Color { - a: 0.5, - ..SURFACE - })), + background: Color { a: 0.5, ..SURFACE }.into(), scroller: scrollable::Scroller { color: HOVERED, ..active.scroller @@ -444,9 +449,9 @@ mod style { slider::Style { rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9 }, + shape: slider::HandleShape::Circle { radius: 9.0 }, color: ACTIVE, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } @@ -482,9 +487,9 @@ mod style { impl progress_bar::StyleSheet for ProgressBar { fn style(&self) -> progress_bar::Style { progress_bar::Style { - background: Background::Color(SURFACE), - bar: Background::Color(ACTIVE), - border_radius: 10, + background: SURFACE.into(), + bar: ACTIVE.into(), + border_radius: 10.0, } } } @@ -494,27 +499,38 @@ mod style { impl checkbox::StyleSheet for Checkbox { fn active(&self, is_checked: bool) -> checkbox::Style { checkbox::Style { - background: Background::Color(if is_checked { - ACTIVE - } else { - SURFACE - }), + background: if is_checked { ACTIVE } else { SURFACE } + .into(), checkmark_color: Color::WHITE, - border_radius: 2, - border_width: 1, + border_radius: 2.0, + border_width: 1.0, border_color: ACTIVE, } } fn hovered(&self, is_checked: bool) -> checkbox::Style { checkbox::Style { - background: Background::Color(Color { + background: Color { a: 0.8, ..if is_checked { ACTIVE } else { SURFACE } - }), + } + .into(), ..self.active(is_checked) } } } + + pub struct Rule; + + impl rule::StyleSheet for Rule { + fn style(&self) -> rule::Style { + rule::Style { + color: SURFACE, + width: 2, + radius: 1.0, + fill_mode: rule::FillMode::Padded(15), + } + } + } } } diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index e19eeca2..8707fa3b 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,6 +1,6 @@ use iced::{Container, Element, Length, Sandbox, Settings, Svg}; -pub fn main() { +pub fn main() -> iced::Result { Tiger::run(Settings::default()) } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index f945cde5..c8926c33 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -6,13 +6,13 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["async-std"] } +iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" -directories = "2.0" +directories-next = "2.0" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Window", "Storage"] } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index c9cbcc69..ccee2703 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,7 +5,7 @@ use iced::{ }; use serde::{Deserialize, Serialize}; -pub fn main() { +pub fn main() -> iced::Result { Todos::run(Settings::default()) } @@ -425,7 +425,7 @@ impl Filter { } } -fn loading_message() -> Element<'static, Message> { +fn loading_message<'a>() -> Element<'a, Message> { Container::new( Text::new("Loading...") .horizontal_alignment(HorizontalAlignment::Center) @@ -437,7 +437,7 @@ fn loading_message() -> Element<'static, Message> { .into() } -fn empty_message(message: &str) -> Element<'static, Message> { +fn empty_message<'a>(message: &str) -> Element<'a, Message> { Container::new( Text::new(message) .width(Length::Fill) @@ -499,7 +499,7 @@ enum SaveError { impl SavedState { fn path() -> std::path::PathBuf { let mut path = if let Some(project_dirs) = - directories::ProjectDirs::from("rs", "Iced", "Todos") + directories_next::ProjectDirs::from("rs", "Iced", "Todos") { project_dirs.data_dir().into() } else { @@ -611,7 +611,7 @@ mod style { background: Some(Background::Color( Color::from_rgb(0.2, 0.2, 0.7), )), - border_radius: 10, + border_radius: 10.0, text_color: Color::WHITE, ..button::Style::default() } @@ -627,7 +627,7 @@ mod style { background: Some(Background::Color(Color::from_rgb( 0.8, 0.2, 0.2, ))), - border_radius: 5, + border_radius: 5.0, text_color: Color::WHITE, shadow_offset: Vector::new(1.0, 1.0), ..button::Style::default() diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 96749e90..bc7fac11 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["image", "debug"] } -env_logger = "0.7" +env_logger = "0.8" diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index c9678b9d..e8755d39 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -4,7 +4,7 @@ use iced::{ Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, }; -pub fn main() { +pub fn main() -> iced::Result { env_logger::init(); Tour::run(Settings::default()) @@ -190,7 +190,7 @@ enum Step { Welcome, Slider { state: slider::State, - value: u16, + value: u8, }, RowsAndColumns { layout: Layout, @@ -222,13 +222,13 @@ enum Step { #[derive(Debug, Clone)] pub enum StepMessage { - SliderChanged(f32), + SliderChanged(u8), LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), + SpacingChanged(u16), + TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageWidthChanged(f32), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -249,12 +249,12 @@ impl<'a> Step { } StepMessage::SliderChanged(new_value) => { if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; + *value = new_value; } } StepMessage::TextSizeChanged(new_size) => { if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; + *size = new_size; } } StepMessage::TextColorChanged(new_color) => { @@ -269,12 +269,12 @@ impl<'a> Step { } StepMessage::SpacingChanged(new_spacing) => { if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; + *spacing = new_spacing; } } StepMessage::ImageWidthChanged(new_width) => { if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -384,7 +384,7 @@ impl<'a> Step { fn slider( state: &'a mut slider::State, - value: u16, + value: u8, ) -> Column<'a, StepMessage> { Self::container("Slider") .push(Text::new( @@ -397,8 +397,8 @@ impl<'a> Step { )) .push(Slider::new( state, - 0.0..=100.0, - value as f32, + 0..=100, + value, StepMessage::SliderChanged, )) .push( @@ -444,8 +444,8 @@ impl<'a> Step { .spacing(10) .push(Slider::new( spacing_slider, - 0.0..=80.0, - spacing as f32, + 0..=80, + spacing, StepMessage::SpacingChanged, )) .push( @@ -486,30 +486,25 @@ impl<'a> Step { ) .push(Slider::new( size_slider, - 10.0..=70.0, - size as f32, + 10..=70, + size, StepMessage::TextSizeChanged, )); let [red, green, blue] = color_sliders; + + let color_sliders = Row::new() + .spacing(10) + .push(color_slider(red, color.r, move |r| Color { r, ..color })) + .push(color_slider(green, color.g, move |g| Color { g, ..color })) + .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + let color_section = Column::new() .padding(20) .spacing(20) .push(Text::new("And its color:")) .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); + .push(color_sliders); Self::container("Text") .push(Text::new( @@ -559,8 +554,8 @@ impl<'a> Step { .push(ferris(width)) .push(Slider::new( slider, - 100.0..=500.0, - width as f32, + 100..=500, + width, StepMessage::ImageWidthChanged, )) .push( @@ -694,7 +689,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { .center_x() } -fn button<'a, Message>( +fn button<'a, Message: Clone>( state: &'a mut button::State, label: &str, ) -> Button<'a, Message> { @@ -706,6 +701,17 @@ fn button<'a, Message>( .min_width(100) } +fn color_slider( + state: &mut slider::State, + component: f32, + update: impl Fn(f32) -> Color + 'static, +) -> Slider<f64, StepMessage> { + Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + StepMessage::TextColorChanged(update(c as f32)) + }) + .step(0.01) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Language { Rust, @@ -763,7 +769,7 @@ mod style { Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), })), - border_radius: 12, + border_radius: 12.0, shadow_offset: Vector::new(1.0, 1.0), text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), ..button::Style::default() diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 275d0391..e8e47c08 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_futures" -version = "0.1.2" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "Commands, subscriptions, and runtimes for Iced" @@ -19,11 +19,17 @@ log = "0.4" [dependencies.futures] version = "0.3" -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio_old] +package = "tokio" version = "0.2" optional = true features = ["rt-core", "rt-threaded", "time", "stream"] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] +version = "0.3" +optional = true +features = ["rt-multi-thread", "time", "stream"] + [target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std] version = "1.0" optional = true diff --git a/futures/src/command.rs b/futures/src/command.rs index 063e9b68..b06ab3f8 100644 --- a/futures/src/command.rs +++ b/futures/src/command.rs @@ -5,9 +5,6 @@ use futures::future::{Future, FutureExt}; /// /// 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 -/// [`Command::perform`]: #method.perform pub struct Command<T> { futures: Vec<BoxFuture<T>>, } @@ -16,8 +13,6 @@ 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(), @@ -25,8 +20,6 @@ impl<T> Command<T> { } /// Creates a [`Command`] that performs the action of the given future. - /// - /// [`Command`]: struct.Command.html #[cfg(not(target_arch = "wasm32"))] pub fn perform<A>( future: impl Future<Output = T> + 'static + Send, @@ -38,8 +31,6 @@ impl<T> Command<T> { } /// Creates a [`Command`] that performs the action of the given future. - /// - /// [`Command`]: struct.Command.html #[cfg(target_arch = "wasm32")] pub fn perform<A>( future: impl Future<Output = T> + 'static, @@ -51,8 +42,6 @@ impl<T> Command<T> { } /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: struct.Command.html #[cfg(not(target_arch = "wasm32"))] pub fn map<A>( mut self, @@ -78,8 +67,6 @@ impl<T> Command<T> { } /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: struct.Command.html #[cfg(target_arch = "wasm32")] pub fn map<A>(mut self, f: impl Fn(T) -> A + 'static) -> Command<A> where @@ -105,8 +92,6 @@ impl<T> Command<T> { /// 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 @@ -117,8 +102,6 @@ impl<T> Command<T> { } /// Converts a [`Command`] into its underlying list of futures. - /// - /// [`Command`]: struct.Command.html pub fn futures(self) -> Vec<BoxFuture<T>> { self.futures } diff --git a/futures/src/executor.rs b/futures/src/executor.rs index cbd34ee8..fa87216a 100644 --- a/futures/src/executor.rs +++ b/futures/src/executor.rs @@ -7,6 +7,9 @@ mod thread_pool; #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] mod tokio; +#[cfg(all(not(target_arch = "wasm32"), feature = "tokio_old"))] +mod tokio_old; + #[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] mod async_std; @@ -21,6 +24,9 @@ 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 = "tokio_old"))] +pub use self::tokio_old::TokioOld; + #[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] pub use self::async_std::AsyncStd; @@ -32,21 +38,15 @@ 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); @@ -56,8 +56,6 @@ pub trait Executor: Sized { /// 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. - /// - /// [`Executor`]: trait.Executor.html fn enter<R>(&self, f: impl FnOnce() -> R) -> R { f() } diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs index a730bce8..c6a21cec 100644 --- a/futures/src/executor/tokio.rs +++ b/futures/src/executor/tokio.rs @@ -16,6 +16,7 @@ impl Executor for Tokio { } fn enter<R>(&self, f: impl FnOnce() -> R) -> R { - tokio::runtime::Runtime::enter(self, f) + let _guard = tokio::runtime::Runtime::enter(self); + f() } } diff --git a/futures/src/executor/tokio_old.rs b/futures/src/executor/tokio_old.rs new file mode 100644 index 00000000..d64729fa --- /dev/null +++ b/futures/src/executor/tokio_old.rs @@ -0,0 +1,21 @@ +use crate::Executor; + +use futures::Future; + +/// An old `tokio` runtime. +#[cfg_attr(docsrs, doc(cfg(feature = "tokio_old")))] +pub type TokioOld = tokio_old::runtime::Runtime; + +impl Executor for TokioOld { + fn new() -> Result<Self, futures::io::Error> { + tokio_old::runtime::Runtime::new() + } + + fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) { + let _ = tokio_old::runtime::Runtime::spawn(self, future); + } + + fn enter<R>(&self, f: impl FnOnce() -> R) -> R { + tokio_old::runtime::Runtime::enter(self, f) + } +} diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 46fc59fc..c7c6fd3a 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -1,4 +1,6 @@ //! Asynchronous tasks for GUI programming, inspired by Elm. +//! +//!  #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] @@ -15,7 +17,7 @@ pub mod executor; pub mod subscription; #[cfg(all( - any(feature = "tokio", feature = "async-std"), + any(feature = "tokio", feature = "tokio_old", feature = "async-std"), not(target_arch = "wasm32") ))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index d204670b..e56a4eb0 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -8,11 +8,6 @@ use std::marker::PhantomData; /// /// 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, @@ -36,8 +31,6 @@ where /// 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, @@ -50,10 +43,6 @@ where /// 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) } @@ -62,9 +51,6 @@ where /// /// 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}; @@ -88,9 +74,7 @@ where /// 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 + /// [`Tracker::update`]: subscription::Tracker::update pub fn track( &mut self, subscription: Subscription<Hasher, Event, Message>, @@ -115,9 +99,7 @@ where /// /// See [`Tracker::broadcast`] to learn more. /// - /// [`Runtime`]: struct.Runtime.html - /// [`Tracker::broadcast`]: - /// subscription/struct.Tracker.html#method.broadcast + /// [`Tracker::broadcast`]: subscription::Tracker::broadcast pub fn broadcast(&mut self, event: Event) { self.subscriptions.broadcast(event); } diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 7a75fc31..27d2d295 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -19,8 +19,7 @@ use crate::BoxStream; /// This type is normally aliased by runtimes with a specific `Event` and/or /// `Hasher`. /// -/// [`Command`]: ../struct.Command.html -/// [`Subscription`]: struct.Subscription.html +/// [`Command`]: crate::Command pub struct Subscription<Hasher, Event, Output> { recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>, } @@ -30,8 +29,6 @@ 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(), @@ -39,9 +36,6 @@ where } /// 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 { @@ -52,8 +46,6 @@ where /// 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 { @@ -66,8 +58,6 @@ where } /// 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 } @@ -75,12 +65,6 @@ where /// Adds a value to the [`Subscription`] context. /// /// The value will be part of the identity of a [`Subscription`]. - /// - /// This is necessary if you want to use multiple instances of the same - /// [`Subscription`] to produce different kinds of messages based on some - /// external data. - /// - /// [`Subscription`]: struct.Subscription.html pub fn with<T>(mut self, value: T) -> Subscription<H, E, (T, O)> where H: 'static, @@ -101,26 +85,19 @@ where } /// 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> + pub fn map<A>(mut self, f: fn(O) -> A) -> 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())) + Box::new(Map::new(recipe, f)) as Box<dyn Recipe<H, E, Output = A>> }) .collect(), @@ -140,9 +117,6 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> { /// by runtimes to run and identify subscriptions. You can use it to create your /// own! /// -/// [`Subscription`]: struct.Subscription.html -/// [`Recipe`]: trait.Recipe.html -/// /// # Examples /// The repository has a couple of [examples] that use a custom [`Recipe`]: /// @@ -151,23 +125,17 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.1/examples/download_progress -/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.1/examples/stopwatch +/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples +/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.2/examples/download_progress +/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.2/examples/stopwatch 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 @@ -175,9 +143,6 @@ pub trait Recipe<Hasher: std::hash::Hasher, Event> { /// /// 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<Event>, @@ -186,13 +151,13 @@ pub trait Recipe<Hasher: std::hash::Hasher, Event> { struct Map<Hasher, Event, A, B> { recipe: Box<dyn Recipe<Hasher, Event, Output = A>>, - mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>, + mapper: fn(A) -> B, } 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>, + mapper: fn(A) -> B, ) -> Self { Map { recipe, mapper } } @@ -209,8 +174,8 @@ where fn hash(&self, state: &mut H) { use std::hash::Hash; - std::any::TypeId::of::<B>().hash(state); self.recipe.hash(state); + self.mapper.hash(state); } fn stream(self: Box<Self>, input: BoxStream<E>) -> BoxStream<Self::Output> { diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index c2a0d0f1..43222b5b 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -26,8 +26,6 @@ where Event: 'static + Send + Clone, { /// Creates a new empty [`Tracker`]. - /// - /// [`Tracker`]: struct.Tracker.html pub fn new() -> Self { Self { subscriptions: HashMap::new(), @@ -52,9 +50,7 @@ where /// 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 + /// [`Recipe`]: crate::subscription::Recipe pub fn update<Message, Receiver>( &mut self, subscription: Subscription<Hasher, Event, Message>, @@ -132,7 +128,7 @@ where /// This method publishes the given event to all the subscription streams /// currently open. /// - /// [`Recipe::stream`]: trait.Recipe.html#tymethod.stream + /// [`Recipe::stream`]: crate::subscription::Recipe::stream pub fn broadcast(&mut self, event: Event) { self.subscriptions .values_mut() diff --git a/futures/src/time.rs b/futures/src/time.rs index e87b4a83..5e9ea436 100644 --- a/futures/src/time.rs +++ b/futures/src/time.rs @@ -5,8 +5,6 @@ use crate::subscription::{self, Subscription}; /// /// The first message is produced after a `duration`, and then continues to /// produce more messages every `duration` after that. -/// -/// [`Subscription`]: ../subscription/struct.Subscription.html pub fn every<H: std::hash::Hasher, E>( duration: std::time::Duration, ) -> Subscription<H, E, std::time::Instant> { @@ -41,7 +39,10 @@ where } } -#[cfg(all(feature = "tokio", not(feature = "async-std")))] +#[cfg(all( + any(feature = "tokio", feature = "tokio_old"), + not(feature = "async-std") +))] impl<H, E> subscription::Recipe<H, E> for Every where H: std::hash::Hasher, @@ -61,6 +62,9 @@ where ) -> futures::stream::BoxStream<'static, Self::Output> { use futures::stream::StreamExt; + #[cfg(feature = "tokio_old")] + use tokio_old as tokio; + let start = tokio::time::Instant::now() + self.0; tokio::time::interval_at(start, self.0) diff --git a/glow/Cargo.toml b/glow/Cargo.toml new file mode 100644 index 00000000..3f85e52d --- /dev/null +++ b/glow/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "iced_glow" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A glow renderer for iced" +license = "MIT AND OFL-1.1" +repository = "https://github.com/hecrj/iced" + +[features] +canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] +default_system_font = ["iced_graphics/font-source"] +# Not supported yet! +image = [] +svg = [] + +[dependencies] +glow = "0.6" +glow_glyph = "0.4" +glyph_brush = "0.7" +euclid = "0.22" +bytemuck = "1.4" +log = "0.4" + +[dependencies.iced_native] +version = "0.3" +path = "../native" + +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" +features = ["font-fallback", "font-icons", "opengl"] + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/glow/src/backend.rs b/glow/src/backend.rs new file mode 100644 index 00000000..92bb993e --- /dev/null +++ b/glow/src/backend.rs @@ -0,0 +1,226 @@ +use crate::quad; +use crate::text; +use crate::triangle; +use crate::{Settings, Transformation, Viewport}; +use iced_graphics::backend; +use iced_graphics::font; +use iced_graphics::Layer; +use iced_graphics::Primitive; +use iced_native::mouse; +use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment}; + +/// A [`glow`] graphics backend for [`iced`]. +/// +/// [`glow`]: https://github.com/grovesNL/glow +/// [`iced`]: https://github.com/hecrj/iced +#[derive(Debug)] +pub struct Backend { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + default_text_size: u16, +} + +impl Backend { + /// Creates a new [`Backend`]. + pub fn new(gl: &glow::Context, settings: Settings) -> Self { + let text_pipeline = text::Pipeline::new(gl, settings.default_font); + let quad_pipeline = quad::Pipeline::new(gl); + let triangle_pipeline = triangle::Pipeline::new(gl); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + default_text_size: settings.default_text_size, + } + } + + /// Draws the provided primitives in the default framebuffer. + /// + /// The text provided as overlay will be rendered on top of the primitives. + /// This is useful for rendering debug information. + pub fn draw<T: AsRef<str>>( + &mut self, + gl: &glow::Context, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + overlay_text: &[T], + ) -> mouse::Interaction { + let viewport_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let projection = viewport.projection(); + + let mut layers = Layer::generate(primitive, viewport); + layers.push(Layer::overlay(overlay_text, viewport)); + + for layer in layers { + self.flush( + gl, + scale_factor, + projection, + &layer, + viewport_size.height, + ); + } + + *mouse_interaction + } + + fn flush( + &mut self, + gl: &glow::Context, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + target_height: u32, + ) { + let mut bounds = (layer.bounds * scale_factor).snap(); + bounds.height = bounds.height.min(target_height); + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + gl, + target_height, + &layer.quads, + transformation, + scale_factor, + bounds, + ); + } + + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.draw( + gl, + target_height, + scaled, + scale_factor, + &layer.meshes, + ); + } + + if !layer.text.is_empty() { + for text in layer.text.iter() { + // Target physical coordinates directly to avoid blurry text + let text = glow_glyph::Section { + // TODO: We `round` here to avoid rerasterizing text when + // its position changes slightly. This can make text feel a + // bit "jumpy". We may be able to do better once we improve + // our text rendering/caching pipeline. + screen_position: ( + (text.bounds.x * scale_factor).round(), + (text.bounds.y * scale_factor).round(), + ), + // 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 + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. + bounds: ( + (text.bounds.width * scale_factor).ceil(), + (text.bounds.height * scale_factor).ceil(), + ), + text: vec![glow_glyph::Text { + text: text.content, + scale: glow_glyph::ab_glyph::PxScale { + x: text.size * scale_factor, + y: text.size * scale_factor, + }, + font_id: self.text_pipeline.find_font(text.font), + extra: glow_glyph::Extra { + color: text.color, + z: 0.0, + }, + }], + layout: glow_glyph::Layout::default() + .h_align(match text.horizontal_alignment { + HorizontalAlignment::Left => { + glow_glyph::HorizontalAlign::Left + } + HorizontalAlignment::Center => { + glow_glyph::HorizontalAlign::Center + } + HorizontalAlignment::Right => { + glow_glyph::HorizontalAlign::Right + } + }) + .v_align(match text.vertical_alignment { + VerticalAlignment::Top => { + glow_glyph::VerticalAlign::Top + } + VerticalAlignment::Center => { + glow_glyph::VerticalAlign::Center + } + VerticalAlignment::Bottom => { + glow_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + gl, + transformation, + glow_glyph::Region { + x: bounds.x, + y: target_height - (bounds.y + bounds.height), + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + self.text_pipeline.trim_measurement_cache() + } +} + +impl backend::Text for Backend { + const ICON_FONT: Font = font::ICONS; + const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; + + fn default_size(&self) -> u16 { + self.default_text_size + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline.measure(contents, size, font, bounds) + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, _handle: &iced_native::image::Handle) -> (u32, u32) { + (50, 50) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + _handle: &iced_native::svg::Handle, + ) -> (u32, u32) { + (50, 50) + } +} diff --git a/glow/src/lib.rs b/glow/src/lib.rs new file mode 100644 index 00000000..98faf24c --- /dev/null +++ b/glow/src/lib.rs @@ -0,0 +1,41 @@ +//! A [`glow`] renderer for [`iced_native`]. +//! +//!  +//! +//! [`glow`]: https://github.com/grovesNL/glow +//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod backend; +mod program; +mod quad; +mod text; +mod triangle; + +pub mod settings; +pub mod widget; +pub mod window; + +pub use backend::Backend; +pub use settings::Settings; + +pub(crate) use iced_graphics::Transformation; + +#[doc(no_inline)] +pub use widget::*; + +pub use iced_graphics::{Error, Viewport}; +pub use iced_native::{ + Background, Color, Command, HorizontalAlignment, Length, Vector, + VerticalAlignment, +}; + +/// A [`glow`] graphics renderer for [`iced`]. +/// +/// [`glow`]: https://github.com/grovesNL/glow +/// [`iced`]: https://github.com/hecrj/iced +pub type Renderer = iced_graphics::Renderer<Backend>; diff --git a/glow/src/program.rs b/glow/src/program.rs new file mode 100644 index 00000000..489a194f --- /dev/null +++ b/glow/src/program.rs @@ -0,0 +1,39 @@ +use glow::HasContext; + +pub unsafe fn create( + gl: &glow::Context, + shader_sources: &[(u32, &str)], +) -> <glow::Context as HasContext>::Program { + let program = gl.create_program().expect("Cannot create program"); + + let mut shaders = Vec::with_capacity(shader_sources.len()); + + for (shader_type, shader_source) in shader_sources.iter() { + let shader = gl + .create_shader(*shader_type) + .expect("Cannot create shader"); + + gl.shader_source(shader, shader_source); + gl.compile_shader(shader); + + if !gl.get_shader_compile_status(shader) { + panic!(gl.get_shader_info_log(shader)); + } + + gl.attach_shader(program, shader); + + shaders.push(shader); + } + + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!(gl.get_program_info_log(program)); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + program +} diff --git a/glow/src/quad.rs b/glow/src/quad.rs new file mode 100644 index 00000000..a8fbb9e5 --- /dev/null +++ b/glow/src/quad.rs @@ -0,0 +1,235 @@ +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use iced_native::Rectangle; + +const MAX_INSTANCES: usize = 100_000; + +#[derive(Debug)] +pub struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + instances: <glow::Context as HasContext>::Buffer, + transform_location: <glow::Context as HasContext>::UniformLocation, + scale_location: <glow::Context as HasContext>::UniformLocation, + screen_height_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, + current_scale: f32, + current_target_height: u32, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/quad.vert")), + (glow::FRAGMENT_SHADER, include_str!("shader/quad.frag")), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + let scale_location = + unsafe { gl.get_uniform_location(program, "u_Scale") } + .expect("Get scale location"); + + let screen_height_location = + unsafe { gl.get_uniform_location(program, "u_ScreenHeight") } + .expect("Get target height location"); + + unsafe { + gl.use_program(Some(program)); + + let matrix: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(&transform_location), + false, + &matrix, + ); + + gl.uniform_1_f32(Some(&scale_location), 1.0); + gl.uniform_1_f32(Some(&screen_height_location), 0.0); + + gl.use_program(None); + } + + let (vertex_array, instances) = + unsafe { create_instance_buffer(gl, MAX_INSTANCES) }; + + Pipeline { + program, + vertex_array, + instances, + transform_location, + scale_location, + screen_height_location, + current_transform: Transformation::identity(), + current_scale: 1.0, + current_target_height: 0, + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + instances: &[layer::Quad], + transformation: Transformation, + scale: f32, + bounds: Rectangle<u32>, + ) { + unsafe { + gl.enable(glow::SCISSOR_TEST); + gl.scissor( + bounds.x as i32, + (target_height - (bounds.y + bounds.height)) as i32, + bounds.width as i32, + bounds.height as i32, + ); + + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instances)); + } + + if transformation != self.current_transform { + unsafe { + let matrix: [f32; 16] = transformation.into(); + gl.uniform_matrix_4_f32_slice( + Some(&self.transform_location), + false, + &matrix, + ); + + self.current_transform = transformation; + } + } + + if scale != self.current_scale { + unsafe { + gl.uniform_1_f32(Some(&self.scale_location), scale); + } + + self.current_scale = scale; + } + + if target_height != self.current_target_height { + unsafe { + gl.uniform_1_f32( + Some(&self.screen_height_location), + target_height as f32, + ); + } + + self.current_target_height = target_height; + } + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + MAX_INSTANCES).min(total); + let amount = end - i; + + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + 0, + bytemuck::cast_slice(&instances[i..end]), + ); + + gl.draw_arrays_instanced( + glow::TRIANGLE_STRIP, + 0, + 4, + amount as i32, + ); + } + + i += MAX_INSTANCES; + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + } + } +} + +unsafe fn create_instance_buffer( + gl: &glow::Context, + size: usize, +) -> ( + <glow::Context as HasContext>::VertexArray, + <glow::Context as HasContext>::Buffer, +) { + let vertex_array = gl.create_vertex_array().expect("Create vertex array"); + let buffer = gl.create_buffer().expect("Create instance buffer"); + + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer)); + gl.buffer_data_size( + glow::ARRAY_BUFFER, + (size * std::mem::size_of::<layer::Quad>()) as i32, + glow::DYNAMIC_DRAW, + ); + + let stride = std::mem::size_of::<layer::Quad>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + gl.vertex_attrib_divisor(0, 1); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2); + gl.vertex_attrib_divisor(1, 1); + + gl.enable_vertex_attrib_array(2); + gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2)); + gl.vertex_attrib_divisor(2, 1); + + gl.enable_vertex_attrib_array(3); + gl.vertex_attrib_pointer_f32( + 3, + 4, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4), + ); + gl.vertex_attrib_divisor(3, 1); + + gl.enable_vertex_attrib_array(4); + gl.vertex_attrib_pointer_f32( + 4, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4), + ); + gl.vertex_attrib_divisor(4, 1); + + gl.enable_vertex_attrib_array(5); + gl.vertex_attrib_pointer_f32( + 5, + 1, + glow::FLOAT, + false, + stride, + 4 * (2 + 2 + 4 + 4 + 1), + ); + gl.vertex_attrib_divisor(5, 1); + + gl.bind_vertex_array(None); + gl.bind_buffer(glow::ARRAY_BUFFER, None); + + (vertex_array, buffer) +} diff --git a/glow/src/settings.rs b/glow/src/settings.rs new file mode 100644 index 00000000..524d91a9 --- /dev/null +++ b/glow/src/settings.rs @@ -0,0 +1,31 @@ +//! Configure a renderer. +pub use iced_graphics::Antialiasing; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// 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 default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + + /// The antialiasing strategy that will be used for triangle primitives. + pub antialiasing: Option<Antialiasing>, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: None, + default_text_size: 20, + antialiasing: None, + } + } +} diff --git a/glow/src/shader/quad.frag b/glow/src/shader/quad.frag new file mode 100644 index 00000000..cea36bdc --- /dev/null +++ b/glow/src/shader/quad.frag @@ -0,0 +1,70 @@ +#version 330 + +uniform float u_ScreenHeight; + +in vec4 v_Color; +in vec4 v_BorderColor; +in vec2 v_Pos; +in vec2 v_Scale; +in float v_BorderRadius; +in float v_BorderWidth; + +out vec4 o_Color; + +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; + + vec2 top_left_distance = top_left - frag_coord; + vec2 bottom_right_distance = frag_coord - bottom_right; + + vec2 distance = vec2( + max(max(top_left_distance.x, bottom_right_distance.x), 0.0), + max(max(top_left_distance.y, bottom_right_distance.y), 0.0) + ); + + return sqrt(distance.x * distance.x + distance.y * distance.y); +} + +void main() { + vec4 mixed_color; + + vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0); + + float internal_distance = distance( + fragCoord, + 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( + fragCoord, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d); + + o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); +} diff --git a/glow/src/shader/quad.vert b/glow/src/shader/quad.vert new file mode 100644 index 00000000..82417856 --- /dev/null +++ b/glow/src/shader/quad.vert @@ -0,0 +1,52 @@ +#version 330 + +uniform mat4 u_Transform; +uniform float u_Scale; + +layout(location = 0) in vec2 i_Pos; +layout(location = 1) in vec2 i_Scale; +layout(location = 2) in vec4 i_Color; +layout(location = 3) in vec4 i_BorderColor; +layout(location = 4) in float i_BorderRadius; +layout(location = 5) in float i_BorderWidth; + +out vec4 v_Color; +out vec4 v_BorderColor; +out vec2 v_Pos; +out vec2 v_Scale; +out float v_BorderRadius; +out float v_BorderWidth; + +const vec2 positions[4] = vec2[]( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + vec2 q_Pos = positions[gl_VertexID]; + vec2 p_Pos = i_Pos * u_Scale; + vec2 p_Scale = i_Scale * u_Scale; + + float i_BorderRadius = min( + i_BorderRadius, + min(i_Scale.x, i_Scale.y) / 2.0 + ); + + mat4 i_Transform = mat4( + vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0) + ); + + v_Color = i_Color; + v_BorderColor = i_BorderColor; + v_Pos = p_Pos; + v_Scale = p_Scale; + v_BorderRadius = i_BorderRadius * u_Scale; + v_BorderWidth = i_BorderWidth * u_Scale; + + gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0); +} diff --git a/glow/src/shader/triangle.frag b/glow/src/shader/triangle.frag new file mode 100644 index 00000000..d186784a --- /dev/null +++ b/glow/src/shader/triangle.frag @@ -0,0 +1,9 @@ +#version 330 + +in vec4 v_Color; + +out vec4 o_Color; + +void main() { + o_Color = v_Color; +} diff --git a/glow/src/shader/triangle.vert b/glow/src/shader/triangle.vert new file mode 100644 index 00000000..5723436a --- /dev/null +++ b/glow/src/shader/triangle.vert @@ -0,0 +1,13 @@ +#version 330 + +uniform mat4 u_Transform; + +layout(location = 0) in vec2 i_Position; +layout(location = 1) in vec4 i_Color; + +out vec4 v_Color; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + v_Color = i_Color; +} diff --git a/glow/src/text.rs b/glow/src/text.rs new file mode 100644 index 00000000..925c7287 --- /dev/null +++ b/glow/src/text.rs @@ -0,0 +1,156 @@ +use crate::Transformation; +use glow_glyph::ab_glyph; +use iced_graphics::font; +use std::{cell::RefCell, collections::HashMap}; + +#[derive(Debug)] +pub struct Pipeline { + draw_brush: RefCell<glow_glyph::GlyphBrush>, + draw_font_map: RefCell<HashMap<String, glow_glyph::FontId>>, + measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, +} + +impl Pipeline { + pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self { + let default_font = default_font.map(|slice| slice.to_vec()); + + // TODO: Font customization + #[cfg(feature = "default_system_font")] + let default_font = { + default_font.or_else(|| { + font::Source::new() + .load(&[font::Family::SansSerif, font::Family::Serif]) + .ok() + }) + }; + + let default_font = + default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); + + let font = ab_glyph::FontArc::try_from_vec(default_font) + .unwrap_or_else(|_| { + log::warn!( + "System font failed to load. Falling back to \ + embedded font..." + ); + + ab_glyph::FontArc::try_from_slice(font::FALLBACK) + .expect("Load fallback font") + }); + + let draw_brush = + glow_glyph::GlyphBrushBuilder::using_font(font.clone()) + .initial_cache_size((2048, 2048)) + .draw_cache_multithread(false) // TODO: Expose as a configuration flag + .build(&gl); + + let measure_brush = + glyph_brush::GlyphBrushBuilder::using_font(font).build(); + + Pipeline { + draw_brush: RefCell::new(draw_brush), + draw_font_map: RefCell::new(HashMap::new()), + measure_brush: RefCell::new(measure_brush), + } + } + + pub fn queue(&mut self, section: glow_glyph::Section<'_>) { + self.draw_brush.borrow_mut().queue(section); + } + + pub fn draw_queued( + &mut self, + gl: &glow::Context, + transformation: Transformation, + region: glow_glyph::Region, + ) { + self.draw_brush + .borrow_mut() + .draw_queued_with_transform_and_scissoring( + gl, + transformation.into(), + region, + ) + .expect("Draw text"); + } + + pub fn measure( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + ) -> (f32, f32) { + use glow_glyph::GlyphCruncher; + + let glow_glyph::FontId(font_id) = self.find_font(font); + + let section = glow_glyph::Section { + bounds: (bounds.width, bounds.height), + text: vec![glow_glyph::Text { + text: content, + scale: size.into(), + font_id: glow_glyph::FontId(font_id), + extra: glow_glyph::Extra::default(), + }], + ..Default::default() + }; + + if let Some(bounds) = + self.measure_brush.borrow_mut().glyph_bounds(section) + { + (bounds.width().ceil(), bounds.height().ceil()) + } else { + (0.0, 0.0) + } + } + + pub fn trim_measurement_cache(&mut self) { + // TODO: We should probably use a `GlyphCalculator` for this. However, + // 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. + 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) -> glow_glyph::FontId { + match font { + iced_native::Font::Default => glow_glyph::FontId(0), + iced_native::Font::External { name, bytes } => { + if let Some(font_id) = self.draw_font_map.borrow().get(name) { + return *font_id; + } + + let font = ab_glyph::FontArc::try_from_slice(bytes) + .expect("Load font"); + + let _ = self.measure_brush.borrow_mut().add_font(font.clone()); + + let font_id = self.draw_brush.borrow_mut().add_font(font); + + let _ = self + .draw_font_map + .borrow_mut() + .insert(String::from(name), font_id); + + font_id + } + } + } +} diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs new file mode 100644 index 00000000..9202bcb2 --- /dev/null +++ b/glow/src/triangle.rs @@ -0,0 +1,292 @@ +//! Draw meshes of triangles. +use crate::program; +use crate::Transformation; +use glow::HasContext; +use iced_graphics::layer; +use std::marker::PhantomData; + +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; + +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; + +#[derive(Debug)] +pub(crate) struct Pipeline { + program: <glow::Context as HasContext>::Program, + vertex_array: <glow::Context as HasContext>::VertexArray, + vertices: Buffer<Vertex2D>, + indices: Buffer<u32>, + transform_location: <glow::Context as HasContext>::UniformLocation, + current_transform: Transformation, +} + +impl Pipeline { + pub fn new(gl: &glow::Context) -> Pipeline { + let program = unsafe { + program::create( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("shader/triangle.vert")), + ( + glow::FRAGMENT_SHADER, + include_str!("shader/triangle.frag"), + ), + ], + ) + }; + + let transform_location = + unsafe { gl.get_uniform_location(program, "u_Transform") } + .expect("Get transform location"); + + unsafe { + gl.use_program(Some(program)); + + let transform: [f32; 16] = Transformation::identity().into(); + gl.uniform_matrix_4_f32_slice( + Some(&transform_location), + false, + &transform, + ); + + gl.use_program(None); + } + + let vertex_array = + unsafe { gl.create_vertex_array().expect("Create vertex array") }; + + unsafe { + gl.bind_vertex_array(Some(vertex_array)); + } + + let vertices = unsafe { + Buffer::new( + gl, + glow::ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + VERTEX_BUFFER_SIZE, + ) + }; + + let indices = unsafe { + Buffer::new( + gl, + glow::ELEMENT_ARRAY_BUFFER, + glow::DYNAMIC_DRAW, + INDEX_BUFFER_SIZE, + ) + }; + + unsafe { + let stride = std::mem::size_of::<Vertex2D>() as i32; + + gl.enable_vertex_attrib_array(0); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); + + gl.enable_vertex_attrib_array(1); + gl.vertex_attrib_pointer_f32( + 1, + 4, + glow::FLOAT, + false, + stride, + 4 * 2, + ); + + gl.bind_vertex_array(None); + } + + Pipeline { + program, + vertex_array, + vertices, + indices, + transform_location, + current_transform: Transformation::identity(), + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + target_height: u32, + transformation: Transformation, + scale_factor: f32, + meshes: &[layer::Mesh<'_>], + ) { + unsafe { + gl.enable(glow::MULTISAMPLE); + gl.enable(glow::SCISSOR_TEST); + gl.use_program(Some(self.program)); + gl.bind_vertex_array(Some(self.vertex_array)); + } + + // 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(|layer::Mesh { buffers, .. }| { + (buffers.vertices.len(), buffers.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 + unsafe { + self.vertices.bind(gl, total_vertices); + self.indices.bind(gl, total_indices); + } + + // We upload all the vertices and indices upfront + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { buffers, .. } in meshes { + unsafe { + gl.buffer_sub_data_u8_slice( + glow::ARRAY_BUFFER, + (last_vertex * std::mem::size_of::<Vertex2D>()) as i32, + bytemuck::cast_slice(&buffers.vertices), + ); + + gl.buffer_sub_data_u8_slice( + glow::ELEMENT_ARRAY_BUFFER, + (last_index * std::mem::size_of::<u32>()) as i32, + bytemuck::cast_slice(&buffers.indices), + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + // Then we draw each mesh using offsets + let mut last_vertex = 0; + let mut last_index = 0; + + for layer::Mesh { + buffers, + origin, + clip_bounds, + } in meshes + { + let transform = + transformation * Transformation::translate(origin.x, origin.y); + + let clip_bounds = (*clip_bounds * scale_factor).snap(); + + unsafe { + if self.current_transform != transform { + let matrix: [f32; 16] = transform.into(); + gl.uniform_matrix_4_f32_slice( + Some(&self.transform_location), + false, + &matrix, + ); + + self.current_transform = transform; + } + + gl.scissor( + clip_bounds.x as i32, + (target_height - (clip_bounds.y + clip_bounds.height)) + as i32, + clip_bounds.width as i32, + clip_bounds.height as i32, + ); + + gl.draw_elements_base_vertex( + glow::TRIANGLES, + buffers.indices.len() as i32, + glow::UNSIGNED_INT, + (last_index * std::mem::size_of::<u32>()) as i32, + last_vertex as i32, + ); + + last_vertex += buffers.vertices.len(); + last_index += buffers.indices.len(); + } + } + + unsafe { + gl.bind_vertex_array(None); + gl.use_program(None); + gl.disable(glow::SCISSOR_TEST); + gl.disable(glow::MULTISAMPLE); + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], +} + +unsafe impl bytemuck::Zeroable for Uniforms {} +unsafe impl bytemuck::Pod for Uniforms {} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + } + } +} + +impl From<Transformation> for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + } + } +} + +#[derive(Debug)] +struct Buffer<T> { + raw: <glow::Context as HasContext>::Buffer, + target: u32, + usage: u32, + size: usize, + phantom: PhantomData<T>, +} + +impl<T> Buffer<T> { + pub unsafe fn new( + gl: &glow::Context, + target: u32, + usage: u32, + size: usize, + ) -> Self { + let raw = gl.create_buffer().expect("Create buffer"); + + let mut buffer = Buffer { + raw, + target, + usage, + size: 0, + phantom: PhantomData, + }; + + buffer.bind(gl, size); + + buffer + } + + pub unsafe fn bind(&mut self, gl: &glow::Context, size: usize) { + gl.bind_buffer(self.target, Some(self.raw)); + + if self.size < size { + gl.buffer_data_size( + self.target, + (size * std::mem::size_of::<T>()) as i32, + self.usage, + ); + + self.size = size; + } + } +} diff --git a/glow/src/widget.rs b/glow/src/widget.rs new file mode 100644 index 00000000..b5c84c56 --- /dev/null +++ b/glow/src/widget.rs @@ -0,0 +1,72 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_glow::{button, Button}; +//! ``` +use crate::Renderer; + +pub mod button; +pub mod checkbox; +pub mod container; +pub mod pane_grid; +pub mod pick_list; +pub mod progress_bar; +pub mod radio; +pub mod rule; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + +pub use iced_native::{Image, Space}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::Text<Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs new file mode 100644 index 00000000..fc729cd5 --- /dev/null +++ b/glow/src/widget/button.rs @@ -0,0 +1,12 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +use crate::Renderer; + +pub use iced_graphics::button::{Style, StyleSheet}; +pub use iced_native::button::State; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>; diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs new file mode 100644 index 00000000..399dd19c --- /dev/null +++ b/glow/src/widget/canvas.rs @@ -0,0 +1,6 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +pub use iced_graphics::canvas::*; diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs new file mode 100644 index 00000000..d27d77cc --- /dev/null +++ b/glow/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_graphics::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>; diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs new file mode 100644 index 00000000..bc26cef2 --- /dev/null +++ b/glow/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_graphics::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>; diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs new file mode 100644 index 00000000..c26dde48 --- /dev/null +++ b/glow/src/widget/pane_grid.rs @@ -0,0 +1,31 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid +use crate::Renderer; + +pub use iced_native::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split, + State, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>; + +/// The content of a [`Pane`]. +pub type Content<'a, Message> = + iced_native::pane_grid::Content<'a, Message, Renderer>; + +/// The title bar of a [`Pane`]. +pub type TitleBar<'a, Message> = + iced_native::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/glow/src/widget/pick_list.rs b/glow/src/widget/pick_list.rs new file mode 100644 index 00000000..fccc68c9 --- /dev/null +++ b/glow/src/widget/pick_list.rs @@ -0,0 +1,9 @@ +//! Display a dropdown list of selectable values. +pub use iced_native::pick_list::State; + +pub use iced_graphics::overlay::menu::Style as Menu; +pub use iced_graphics::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message> = + iced_native::PickList<'a, T, Message, crate::Renderer>; diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs new file mode 100644 index 00000000..45a25d00 --- /dev/null +++ b/glow/src/widget/progress_bar.rs @@ -0,0 +1,13 @@ +//! Allow your users to visually track the progress of a computation. +//! +//! A [`ProgressBar`] has a range of possible values and a current value, +//! as well as a length, height and style. +use crate::Renderer; + +pub use iced_graphics::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/glow/src/widget/qr_code.rs b/glow/src/widget/qr_code.rs new file mode 100644 index 00000000..7b1c2408 --- /dev/null +++ b/glow/src/widget/qr_code.rs @@ -0,0 +1,2 @@ +//! Encode and display information in a QR code. +pub use iced_graphics::qr_code::*; diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs new file mode 100644 index 00000000..0b843d1f --- /dev/null +++ b/glow/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_graphics::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message> = iced_native::Radio<Message, Renderer>; diff --git a/glow/src/widget/rule.rs b/glow/src/widget/rule.rs new file mode 100644 index 00000000..faa2be86 --- /dev/null +++ b/glow/src/widget/rule.rs @@ -0,0 +1,10 @@ +//! Display a horizontal or vertical rule for dividing content. + +use crate::Renderer; + +pub use iced_graphics::rule::{FillMode, Style, StyleSheet}; + +/// Display a horizontal or vertical rule for dividing content. +/// +/// This is an alias of an `iced_native` rule with an `iced_glow::Renderer`. +pub type Rule = iced_native::Rule<Renderer>; diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs new file mode 100644 index 00000000..fabb4318 --- /dev/null +++ b/glow/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; +pub use iced_native::scrollable::State; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs new file mode 100644 index 00000000..9a269858 --- /dev/null +++ b/glow/src/widget/slider.rs @@ -0,0 +1,13 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::Renderer; + +pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; +pub use iced_native::slider::State; + +/// 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, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs new file mode 100644 index 00000000..db18b1cc --- /dev/null +++ b/glow/src/widget/text_input.rs @@ -0,0 +1,12 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +use crate::Renderer; + +pub use iced_graphics::text_input::{Style, StyleSheet}; +pub use iced_native::text_input::State; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>; diff --git a/glow/src/window.rs b/glow/src/window.rs new file mode 100644 index 00000000..aac5fb9e --- /dev/null +++ b/glow/src/window.rs @@ -0,0 +1,4 @@ +//! Display rendering results on windows. +mod compositor; + +pub use compositor::Compositor; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs new file mode 100644 index 00000000..1fb671ad --- /dev/null +++ b/glow/src/window/compositor.rs @@ -0,0 +1,76 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use core::ffi::c_void; +use glow::HasContext; +use iced_graphics::{Antialiasing, Size}; +use iced_native::mouse; + +/// A window graphics backend for iced powered by `glow`. +#[allow(missing_debug_implementations)] +pub struct Compositor { + gl: glow::Context, +} + +impl iced_graphics::window::GLCompositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> Result<(Self, Self::Renderer), Error> { + let gl = glow::Context::from_loader_function(loader_function); + + // Enable auto-conversion from/to sRGB + gl.enable(glow::FRAMEBUFFER_SRGB); + + // Enable alpha blending + gl.enable(glow::BLEND); + gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); + + // Disable multisampling by default + gl.disable(glow::MULTISAMPLE); + + let renderer = Renderer::new(Backend::new(&gl, settings)); + + Ok((Self { gl }, renderer)) + } + + fn sample_count(settings: &Settings) -> u32 { + settings + .antialiasing + .map(Antialiasing::sample_count) + .unwrap_or(0) + } + + fn resize_viewport(&mut self, physical_size: Size<u32>) { + unsafe { + self.gl.viewport( + 0, + 0, + physical_size.width as i32, + physical_size.height as i32, + ); + } + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction { + let gl = &self.gl; + + let [r, g, b, a] = color.into_linear(); + + unsafe { + gl.clear_color(r, g, b, a); + gl.clear(glow::COLOR_BUFFER_BIT); + } + + renderer.backend_mut().draw(gl, viewport, output, overlay) + } +} diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml new file mode 100644 index 00000000..505ee7e5 --- /dev/null +++ b/glutin/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "iced_glutin" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A glutin runtime for Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_glutin" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] + +[features] +debug = ["iced_winit/debug"] + +[dependencies] +glutin = "0.26" + +[dependencies.iced_native] +version = "0.3" +path = "../native" + +[dependencies.iced_winit] +version = "0.2" +path = "../winit" + +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" +features = ["opengl"] diff --git a/glutin/README.md b/glutin/README.md new file mode 100644 index 00000000..addb9228 --- /dev/null +++ b/glutin/README.md @@ -0,0 +1,29 @@ +# `iced_glutin` +[][documentation] +[](https://crates.io/crates/iced_glutin) +[](https://github.com/hecrj/iced/blob/master/LICENSE) +[](https://iced.zulipchat.com) + +`iced_glutin` offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`glutin`]. + +It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop. + +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> + +[documentation]: https://docs.rs/iced_glutin +[`iced_native`]: ../native +[`glutin`]: https://github.com/rust-windowing/glutin + +## Installation +Add `iced_glutin` as a dependency in your `Cargo.toml`: + +```toml +iced_glutin = "0.1" +``` + +__Iced moves fast and the `master` branch can contain breaking changes!__ If +you want to learn about a specific release, check out [the release list]. + +[the release list]: https://github.com/hecrj/iced/releases diff --git a/glutin/src/application.rs b/glutin/src/application.rs new file mode 100644 index 00000000..42513feb --- /dev/null +++ b/glutin/src/application.rs @@ -0,0 +1,297 @@ +//! Create interactive, native cross-platform applications. +use crate::{mouse, Error, Executor, Runtime}; + +pub use iced_winit::Application; + +use iced_graphics::window; +use iced_winit::application; +use iced_winit::conversion; +use iced_winit::futures; +use iced_winit::futures::channel::mpsc; +use iced_winit::{Cache, Clipboard, Debug, Proxy, Settings}; + +use glutin::window::Window; +use std::mem::ManuallyDrop; + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: window::GLCompositor<Renderer = A::Renderer> + 'static, +{ + use futures::task; + use futures::Future; + use glutin::event_loop::EventLoop; + use glutin::ContextBuilder; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoop::with_user_event(); + let mut runtime = { + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + let proxy = Proxy::new(event_loop.create_proxy()); + + Runtime::new(executor, proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let subscription = application.subscription(); + + runtime.spawn(init_command); + runtime.track(subscription); + + let context = { + let builder = settings.window.into_builder( + &application.title(), + application.mode(), + event_loop.primary_monitor(), + ); + + let context = ContextBuilder::new() + .with_vsync(true) + .with_multisampling(C::sample_count(&compositor_settings) as u16) + .build_windowed(builder, &event_loop) + .map_err(|error| { + use glutin::CreationError; + + match error { + CreationError::Window(error) => { + Error::WindowCreationFailed(error) + } + _ => Error::GraphicsAdapterNotFound, + } + })?; + + #[allow(unsafe_code)] + unsafe { + context.make_current().expect("Make OpenGL context current") + } + }; + + #[allow(unsafe_code)] + let (compositor, renderer) = unsafe { + C::new(compositor_settings, |address| { + context.get_proc_address(address) + })? + }; + + let (mut sender, receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + renderer, + context, + runtime, + debug, + receiver, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + event_loop.run(move |event, _, control_flow| { + use glutin::event_loop::ControlFlow; + + if let ControlFlow::Exit = control_flow { + return; + } + + if let Some(event) = event.to_static() { + sender.start_send(event).expect("Send event"); + + let poll = instance.as_mut().poll(&mut context); + + *control_flow = match poll { + task::Poll::Pending => ControlFlow::Wait, + task::Poll::Ready(_) => ControlFlow::Exit, + }; + } + }); +} + +async fn run_instance<A, E, C>( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>, + mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver<glutin::event::Event<'_, A::Message>>, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::GLCompositor<Renderer = A::Renderer> + 'static, +{ + use glutin::event; + use iced_winit::futures::stream::StreamExt; + + let clipboard = Clipboard::new(context.window()); + + let mut state = application::State::new(&application, context.window()); + let mut viewport_version = state.viewport_version(); + let mut user_interface = + ManuallyDrop::new(application::build_user_interface( + &mut application, + Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + )); + + let mut primitive = + user_interface.draw(&mut renderer, state.cursor_position()); + let mut mouse_interaction = mouse::Interaction::default(); + + let mut events = Vec::new(); + let mut messages = Vec::new(); + + debug.startup_finished(); + + while let Some(event) = receiver.next().await { + match event { + event::Event::MainEventsCleared => { + if events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + + let statuses = user_interface.update( + &events, + state.cursor_position(), + clipboard.as_ref().map(|c| c as _), + &mut renderer, + &mut messages, + ); + + debug.event_processing_finished(); + + for event in events.drain(..).zip(statuses.into_iter()) { + runtime.broadcast(event); + } + + if !messages.is_empty() { + let cache = + ManuallyDrop::into_inner(user_interface).into_cache(); + + // Update application + application::update( + &mut application, + &mut runtime, + &mut debug, + &mut messages, + ); + + // Update window + state.synchronize(&application, context.window()); + + user_interface = + ManuallyDrop::new(application::build_user_interface( + &mut application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); + } + + debug.draw_started(); + primitive = + user_interface.draw(&mut renderer, state.cursor_position()); + debug.draw_finished(); + + context.window().request_redraw(); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::RedrawRequested(_) => { + debug.render_started(); + let current_viewport_version = state.viewport_version(); + + if viewport_version != current_viewport_version { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); + + debug.layout_started(); + user_interface = ManuallyDrop::new( + ManuallyDrop::into_inner(user_interface) + .relayout(logical_size, &mut renderer), + ); + debug.layout_finished(); + + debug.draw_started(); + primitive = user_interface + .draw(&mut renderer, state.cursor_position()); + debug.draw_finished(); + + context.resize(glutin::dpi::PhysicalSize::new( + physical_size.width, + physical_size.height, + )); + + compositor.resize_viewport(physical_size); + + viewport_version = current_viewport_version; + } + + let new_mouse_interaction = compositor.draw( + &mut renderer, + state.viewport(), + state.background_color(), + &primitive, + &debug.overlay(), + ); + + context.swap_buffers().expect("Swap buffers"); + + debug.render_finished(); + + if new_mouse_interaction != mouse_interaction { + context.window().set_cursor_icon( + conversion::mouse_interaction(new_mouse_interaction), + ); + + mouse_interaction = new_mouse_interaction; + } + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + event::Event::WindowEvent { + event: window_event, + .. + } => { + if application::requests_exit(&window_event, state.modifiers()) + { + break; + } + + state.update(context.window(), &window_event, &mut debug); + + if let Some(event) = conversion::window_event( + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events.push(event); + } + } + _ => {} + } + } + + // Manually drop the user interface + drop(ManuallyDrop::into_inner(user_interface)); +} diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs new file mode 100644 index 00000000..f2c0102a --- /dev/null +++ b/glutin/src/lib.rs @@ -0,0 +1,25 @@ +//! A windowing shell for [`iced`], on top of [`glutin`]. +//! +//!  +//! +//! [`iced`]: https://github.com/hecrj/iced +//! [`glutin`]: https://github.com/rust-windowing/glutin +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![forbid(rust_2018_idioms)] + +pub use glutin; +#[doc(no_inline)] +pub use iced_native::*; + +pub mod application; + +pub use iced_winit::settings; +pub use iced_winit::{Error, Mode}; + +#[doc(no_inline)] +pub use application::Application; +#[doc(no_inline)] +pub use settings::Settings; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml new file mode 100644 index 00000000..73dc47bf --- /dev/null +++ b/graphics/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "iced_graphics" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" +license = "MIT" +repository = "https://github.com/hecrj/iced" +documentation = "https://docs.rs/iced_graphics" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] + +[features] +canvas = ["lyon"] +qr_code = ["qrcode", "canvas"] +font-source = ["font-kit"] +font-fallback = [] +font-icons = [] +opengl = [] + +[dependencies] +glam = "0.10" +raw-window-handle = "0.3" +thiserror = "1.0" + +[dependencies.bytemuck] +version = "1.4" +features = ["derive"] + +[dependencies.iced_native] +version = "0.3" +path = "../native" + +[dependencies.iced_style] +version = "0.2" +path = "../style" + +[dependencies.lyon] +version = "0.16" +optional = true + +[dependencies.qrcode] +version = "0.12" +optional = true + +[dependencies.font-kit] +version = "0.8" +optional = true + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true diff --git a/wgpu/src/text/icons.ttf b/graphics/fonts/Icons.ttf Binary files differindex 1c832f86..5e455b69 100644 --- a/wgpu/src/text/icons.ttf +++ b/graphics/fonts/Icons.ttf diff --git a/wgpu/fonts/Lato-Regular.ttf b/graphics/fonts/Lato-Regular.ttf Binary files differindex 33eba8b1..33eba8b1 100644 --- a/wgpu/fonts/Lato-Regular.ttf +++ b/graphics/fonts/Lato-Regular.ttf diff --git a/wgpu/fonts/OFL.txt b/graphics/fonts/OFL.txt index dfca0da4..dfca0da4 100644 --- a/wgpu/fonts/OFL.txt +++ b/graphics/fonts/OFL.txt diff --git a/graphics/src/antialiasing.rs b/graphics/src/antialiasing.rs new file mode 100644 index 00000000..7631c97c --- /dev/null +++ b/graphics/src/antialiasing.rs @@ -0,0 +1,24 @@ +/// 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 { + /// Returns the amount of samples of the [`Antialiasing`]. + pub fn sample_count(self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs new file mode 100644 index 00000000..ed1b9e08 --- /dev/null +++ b/graphics/src/backend.rs @@ -0,0 +1,58 @@ +//! Write a graphics backend. +use iced_native::image; +use iced_native::svg; +use iced_native::{Font, Size}; + +/// The graphics backend of a [`Renderer`]. +/// +/// [`Renderer`]: crate::Renderer +pub trait Backend { + /// Trims the measurements cache. + /// + /// This method is currently necessary to properly trim the text cache in + /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering + /// pipeline. It will be removed in the future. + fn trim_measurements(&mut self) {} +} + +/// A graphics backend that supports text rendering. +pub trait Text { + /// The icon font of the backend. + const ICON_FONT: Font; + + /// The `char` representing a ✔ icon in the [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const CHECKMARK_ICON: char; + + /// The `char` representing a â–¼ icon in the built-in [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const ARROW_DOWN_ICON: char; + + /// Returns the default size of text. + fn default_size(&self) -> u16; + + /// Measures the text contents with the given size and font, + /// returning the size of a laid out paragraph that fits in the provided + /// bounds. + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32); +} + +/// A graphics backend that supports image rendering. +pub trait Image { + /// Returns the dimensions of the provided image. + fn dimensions(&self, handle: &image::Handle) -> (u32, u32); +} + +/// A graphics backend that supports SVG rendering. +pub trait Svg { + /// Returns the viewport dimensions of the provided SVG. + fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32); +} diff --git a/wgpu/src/defaults.rs b/graphics/src/defaults.rs index 11718a87..11718a87 100644 --- a/wgpu/src/defaults.rs +++ b/graphics/src/defaults.rs diff --git a/graphics/src/error.rs b/graphics/src/error.rs new file mode 100644 index 00000000..c86e326a --- /dev/null +++ b/graphics/src/error.rs @@ -0,0 +1,7 @@ +/// A graphical error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// A suitable graphics adapter or device could not be found + #[error("a suitable graphics adapter or device could not be found")] + AdapterNotFound, +} diff --git a/graphics/src/font.rs b/graphics/src/font.rs new file mode 100644 index 00000000..d55d0faf --- /dev/null +++ b/graphics/src/font.rs @@ -0,0 +1,35 @@ +//! Find system fonts or use the built-in ones. +#[cfg(feature = "font-source")] +mod source; + +#[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] +pub use source::Source; + +#[cfg(feature = "font-source")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; + +/// A built-in fallback font, for convenience. +#[cfg(feature = "font-fallback")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))] +pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); + +/// A built-in icon font, for convenience. +#[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] +pub const ICONS: iced_native::Font = iced_native::Font::External { + name: "iced_wgpu icons", + bytes: include_bytes!("../fonts/Icons.ttf"), +}; + +/// The `char` representing a ✔ icon in the built-in [`ICONS`] font. +#[cfg(feature = "font-icons")] +#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] +pub const CHECKMARK_ICON: char = '\u{F00C}'; + +/// The `char` representing a â–¼ icon in the built-in [`ICONS`] font. +#[cfg(feature = "font-icons")] +pub const ARROW_DOWN_ICON: char = '\u{E800}'; diff --git a/wgpu/src/text/font.rs b/graphics/src/font/source.rs index 7346ccdb..a2d3f51d 100644 --- a/wgpu/src/text/font.rs +++ b/graphics/src/font/source.rs @@ -1,18 +1,20 @@ -pub use font_kit::{ - error::SelectionError as LoadError, family_name::FamilyName as Family, -}; +use crate::font::{Family, LoadError}; +/// A font source that can find and load system fonts. +#[allow(missing_debug_implementations)] pub struct Source { raw: font_kit::source::SystemSource, } impl Source { + /// Creates a new [`Source`]. pub fn new() -> Self { Source { raw: font_kit::source::SystemSource::new(), } } + /// Finds and loads a font matching the set of provided family priorities. pub fn load(&self, families: &[Family]) -> Result<Vec<u8>, LoadError> { let font = self.raw.select_best_match( families, diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..ab40b114 --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,311 @@ +//! Organize rendering primitives into a flattened list of layers. +use crate::image; +use crate::svg; +use crate::triangle; +use crate::{ + Background, Font, HorizontalAlignment, Point, Primitive, Rectangle, Size, + Vector, VerticalAlignment, Viewport, +}; + +/// A group of primitives that should be clipped together. +#[derive(Debug, Clone)] +pub struct Layer<'a> { + /// The clipping bounds of the [`Layer`]. + pub bounds: Rectangle, + + /// The quads of the [`Layer`]. + pub quads: Vec<Quad>, + + /// The triangle meshes of the [`Layer`]. + pub meshes: Vec<Mesh<'a>>, + + /// The text of the [`Layer`]. + pub text: Vec<Text<'a>>, + + /// The images of the [`Layer`]. + pub images: Vec<Image>, +} + +impl<'a> Layer<'a> { + /// Creates a new [`Layer`] with the given clipping bounds. + pub fn new(bounds: Rectangle) -> Self { + Self { + bounds, + quads: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } + + /// Creates a new [`Layer`] for the provided overlay text. + /// + /// This can be useful for displaying debug information. + pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self { + let mut overlay = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + for (i, line) in lines.iter().enumerate() { + let text = Text { + content: line.as_ref(), + bounds: Rectangle::new( + Point::new(11.0, 11.0 + 25.0 * i as f32), + Size::INFINITY, + ), + color: [0.9, 0.9, 0.9, 1.0], + size: 20.0, + font: Font::Default, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Top, + }; + + overlay.text.push(text); + + overlay.text.push(Text { + bounds: text.bounds + Vector::new(-1.0, -1.0), + color: [0.0, 0.0, 0.0, 1.0], + ..text + }); + } + + overlay + } + + /// Distributes the given [`Primitive`] and generates a list of layers based + /// on its contents. + pub fn generate( + primitive: &'a Primitive, + viewport: &Viewport, + ) -> Vec<Self> { + let first_layer = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + let mut layers = vec![first_layer]; + + Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive); + + layers + } + + fn process_primitive( + layers: &mut Vec<Self>, + translation: Vector, + primitive: &'a Primitive, + ) { + match primitive { + Primitive::None => {} + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + Self::process_primitive(layers, translation, primitive) + } + } + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = layers.last_mut().unwrap(); + + layer.text.push(Text { + content, + bounds: *bounds + translation, + size: *size, + color: color.into_linear(), + font: *font, + horizontal_alignment: *horizontal_alignment, + vertical_alignment: *vertical_alignment, + }); + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = layers.last_mut().unwrap(); + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius, + border_width: *border_width, + border_color: border_color.into_linear(), + }); + } + Primitive::Mesh2D { buffers, size } => { + let layer = layers.last_mut().unwrap(); + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + }); + } + } + Primitive::Clip { + bounds, + offset, + content, + } => { + let layer = layers.last_mut().unwrap(); + let translated_bounds = *bounds + translation; + + // Only draw visible content + if let Some(clip_bounds) = + layer.bounds.intersection(&translated_bounds) + { + let clip_layer = Layer::new(clip_bounds); + let new_layer = Layer::new(layer.bounds); + + layers.push(clip_layer); + Self::process_primitive( + layers, + translation + - Vector::new(offset.x as f32, offset.y as f32), + content, + ); + layers.push(new_layer); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + Self::process_primitive( + layers, + translation + *new_translation, + &content, + ); + } + Primitive::Cached { cache } => { + Self::process_primitive(layers, translation, &cache); + } + Primitive::Image { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Raster { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + Primitive::Svg { handle, bounds } => { + let layer = layers.last_mut().unwrap(); + + layer.images.push(Image::Vector { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + } + } +} + +/// A colored rectangle with a border. +/// +/// This type can be directly uploaded to GPU memory. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + pub border_radius: f32, + + /// The border width of the [`Quad`]. + pub border_width: f32, +} + +/// A mesh of triangles. +#[derive(Debug, Clone, Copy)] +pub struct Mesh<'a> { + /// The origin of the vertices of the [`Mesh`]. + pub origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + pub buffers: &'a triangle::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + pub clip_bounds: Rectangle<f32>, +} + +/// A paragraph of text. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a> { + /// The content of the [`Text`]. + pub content: &'a str, + + /// The layout bounds of the [`Text`]. + pub bounds: Rectangle, + + /// The color of the [`Text`], in __linear RGB_. + pub color: [f32; 4], + + /// The size of the [`Text`]. + pub size: f32, + + /// The font of the [`Text`]. + pub font: Font, + + /// The horizontal alignment of the [`Text`]. + pub horizontal_alignment: HorizontalAlignment, + + /// The vertical alignment of the [`Text`]. + pub vertical_alignment: VerticalAlignment, +} + +/// A raster or vector image. +#[derive(Debug, Clone)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The bounds of the image. + bounds: Rectangle, + }, +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs new file mode 100644 index 00000000..14388653 --- /dev/null +++ b/graphics/src/lib.rs @@ -0,0 +1,45 @@ +//! A bunch of backend-agnostic types that can be leveraged to build a renderer +//! for [`iced`]. +//! +//!  +//! +//! [`iced`]: https://github.com/hecrj/iced +#![deny(missing_docs)] +#![deny(missing_debug_implementations)] +#![deny(unused_results)] +#![deny(unsafe_code)] +#![forbid(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] +mod antialiasing; +mod error; +mod primitive; +mod renderer; +mod transformation; +mod viewport; + +pub mod backend; +pub mod defaults; +pub mod font; +pub mod layer; +pub mod overlay; +pub mod triangle; +pub mod widget; +pub mod window; + +#[doc(no_inline)] +pub use widget::*; + +pub use antialiasing::Antialiasing; +pub use backend::Backend; +pub use defaults::Defaults; +pub use error::Error; +pub use layer::Layer; +pub use primitive::Primitive; +pub use renderer::Renderer; +pub use transformation::Transformation; +pub use viewport::Viewport; + +pub use iced_native::{ + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, +}; diff --git a/graphics/src/overlay.rs b/graphics/src/overlay.rs new file mode 100644 index 00000000..bc0ed744 --- /dev/null +++ b/graphics/src/overlay.rs @@ -0,0 +1,2 @@ +//! Display interactive elements on top of other widgets. +pub mod menu; diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs new file mode 100644 index 00000000..ffe998c5 --- /dev/null +++ b/graphics/src/overlay/menu.rs @@ -0,0 +1,117 @@ +//! Build and show dropdown menus. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, overlay, Color, Font, HorizontalAlignment, Point, Rectangle, + VerticalAlignment, +}; + +pub use iced_style::menu::Style; + +impl<B> overlay::menu::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Style; + + fn decorate( + &mut self, + bounds: Rectangle, + _cursor_position: Point, + style: &Style, + (primitives, mouse_cursor): Self::Output, + ) -> Self::Output { + ( + Primitive::Group { + primitives: vec![ + Primitive::Quad { + bounds, + background: style.background, + border_color: style.border_color, + border_width: style.border_width, + border_radius: 0.0, + }, + primitives, + ], + }, + mouse_cursor, + ) + } + + fn draw<T: ToString>( + &mut self, + bounds: Rectangle, + cursor_position: Point, + viewport: &Rectangle, + options: &[T], + hovered_option: Option<usize>, + padding: u16, + text_size: u16, + font: Font, + style: &Style, + ) -> Self::Output { + use std::f32; + + let is_mouse_over = bounds.contains(cursor_position); + let option_height = text_size as usize + padding as usize * 2; + + let mut primitives = Vec::new(); + + let offset = viewport.y - bounds.y; + let start = (offset / option_height as f32) as usize; + let end = + ((offset + viewport.height) / option_height as f32).ceil() as usize; + + let visible_options = &options[start..end.min(options.len())]; + + for (i, option) in visible_options.iter().enumerate() { + let i = start + i; + let is_selected = hovered_option == Some(i); + + let bounds = Rectangle { + x: bounds.x, + y: bounds.y + (option_height * i) as f32, + width: bounds.width, + height: f32::from(text_size + padding * 2), + }; + + if is_selected { + primitives.push(Primitive::Quad { + bounds, + background: style.selected_background, + border_color: Color::TRANSPARENT, + border_width: 0.0, + border_radius: 0.0, + }); + } + + primitives.push(Primitive::Text { + content: option.to_string(), + bounds: Rectangle { + x: bounds.x + f32::from(padding), + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }, + size: f32::from(text_size), + font, + color: if is_selected { + style.selected_text_color + } else { + style.text_color + }, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }); + } + + ( + Primitive::Group { primitives }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/wgpu/src/primitive.rs b/graphics/src/primitive.rs index e73227ef..30263bd4 100644 --- a/wgpu/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -40,9 +40,9 @@ pub enum Primitive { /// The background of the quad background: Background, /// The border radius of the quad - border_radius: u16, + border_radius: f32, /// The border width of the quad - border_width: u16, + border_width: f32, /// The border color of the quad border_color: Color, }, @@ -82,13 +82,13 @@ pub enum Primitive { /// /// It can be used to render many kinds of geometry freely. Mesh2D { + /// The vertex and index buffers of the mesh + buffers: triangle::Mesh2D, + /// The size of the drawable region of the mesh. /// /// Any geometry that falls out of this region will be clipped. size: Size, - - /// The vertex and index buffers of the mesh - buffers: triangle::Mesh2D, }, /// A cached primitive. /// diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs new file mode 100644 index 00000000..fa63991b --- /dev/null +++ b/graphics/src/renderer.rs @@ -0,0 +1,121 @@ +use crate::{Backend, Defaults, Primitive}; +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::{ + Background, Color, Element, Point, Rectangle, Vector, Widget, +}; + +/// A backend-agnostic renderer that supports all the built-in widgets. +#[derive(Debug)] +pub struct Renderer<B: Backend> { + backend: B, +} + +impl<B: Backend> Renderer<B> { + /// Creates a new [`Renderer`] from the given [`Backend`]. + pub fn new(backend: B) -> Self { + Self { backend } + } + + /// Returns a reference to the [`Backend`] of the [`Renderer`]. + pub fn backend(&self) -> &B { + &self.backend + } + + /// Returns a mutable reference to the [`Backend`] of the [`Renderer`]. + pub fn backend_mut(&mut self) -> &mut B { + &mut self.backend + } +} + +impl<B> iced_native::Renderer for Renderer<B> +where + B: Backend, +{ + type Output = (Primitive, mouse::Interaction); + type Defaults = Defaults; + + fn layout<'a, Message>( + &mut self, + element: &Element<'a, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + let layout = element.layout(self, limits); + + self.backend.trim_measurements(); + + layout + } + + fn overlay( + &mut self, + (base_primitive, base_cursor): (Primitive, mouse::Interaction), + (overlay_primitives, overlay_cursor): (Primitive, mouse::Interaction), + overlay_bounds: Rectangle, + ) -> (Primitive, mouse::Interaction) { + ( + Primitive::Group { + primitives: vec![ + base_primitive, + Primitive::Clip { + bounds: Rectangle { + width: overlay_bounds.width + 0.5, + height: overlay_bounds.height + 0.5, + ..overlay_bounds + }, + offset: Vector::new(0, 0), + content: Box::new(overlay_primitives), + }, + ], + }, + if base_cursor > overlay_cursor { + base_cursor + } else { + overlay_cursor + }, + ) + } +} + +impl<B> layout::Debugger for Renderer<B> +where + B: Backend, +{ + fn explain<Message>( + &mut self, + defaults: &Defaults, + widget: &dyn Widget<Message, Self>, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + color: Color, + ) -> Self::Output { + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position, viewport); + + let mut primitives = Vec::new(); + + explain_layout(layout, color, &mut primitives); + primitives.push(primitive); + + (Primitive::Group { primitives }, cursor) + } +} + +fn explain_layout( + layout: Layout<'_>, + color: Color, + primitives: &mut Vec<Primitive>, +) { + primitives.push(Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::TRANSPARENT), + border_radius: 0.0, + border_width: 1.0, + border_color: [0.6, 0.6, 0.6, 0.5].into(), + }); + + for child in layout.children() { + explain_layout(child, color, primitives); + } +} diff --git a/wgpu/src/transformation.rs b/graphics/src/transformation.rs index ff3b1d00..2a19caed 100644 --- a/wgpu/src/transformation.rs +++ b/graphics/src/transformation.rs @@ -1,4 +1,4 @@ -use glam::{Mat4, Vec3, Vec4}; +use glam::{Mat4, Vec3}; use std::ops::Mul; /// A 2D transformation matrix. @@ -14,11 +14,10 @@ impl Transformation { /// Creates an orthographic projection. #[rustfmt::skip] pub fn orthographic(width: u32, height: u32) -> Transformation { - Transformation(Mat4::from_cols( - 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) + Transformation(Mat4::orthographic_rh_gl( + 0.0, width as f32, + height as f32, 0.0, + -1.0, 1.0 )) } diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs new file mode 100644 index 00000000..05028f51 --- /dev/null +++ b/graphics/src/triangle.rs @@ -0,0 +1,25 @@ +//! Draw geometry using meshes of triangles. +use bytemuck::{Pod, Zeroable}; + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +#[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>, +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs new file mode 100644 index 00000000..78d539af --- /dev/null +++ b/graphics/src/viewport.rs @@ -0,0 +1,56 @@ +use crate::{Size, Transformation}; + +/// A viewing region for displaying computer graphics. +#[derive(Debug, Clone)] +pub struct Viewport { + physical_size: Size<u32>, + logical_size: Size<f32>, + scale_factor: f64, + projection: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given physical dimensions and scale + /// factor. + pub fn with_physical_size(size: Size<u32>, scale_factor: f64) -> Viewport { + Viewport { + physical_size: size, + logical_size: Size::new( + (size.width as f64 / scale_factor) as f32, + (size.height as f64 / scale_factor) as f32, + ), + scale_factor, + projection: Transformation::orthographic(size.width, size.height), + } + } + + /// Returns the physical size of the [`Viewport`]. + pub fn physical_size(&self) -> Size<u32> { + self.physical_size + } + + /// Returns the physical width of the [`Viewport`]. + pub fn physical_width(&self) -> u32 { + self.physical_size.height + } + + /// Returns the physical height of the [`Viewport`]. + pub fn physical_height(&self) -> u32 { + self.physical_size.height + } + + /// Returns the logical size of the [`Viewport`]. + pub fn logical_size(&self) -> Size<f32> { + self.logical_size + } + + /// Returns the scale factor of the [`Viewport`]. + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Returns the projection transformation of the [`Viewport`]. + pub fn projection(&self) -> Transformation { + self.projection + } +} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs new file mode 100644 index 00000000..159ca91b --- /dev/null +++ b/graphics/src/widget.rs @@ -0,0 +1,73 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_graphics::{button, Button}; +//! ``` +pub mod button; +pub mod checkbox; +pub mod container; +pub mod image; +pub mod pane_grid; +pub mod pick_list; +pub mod progress_bar; +pub mod radio; +pub mod rule; +pub mod scrollable; +pub mod slider; +pub mod svg; +pub mod text_input; + +mod column; +mod row; +mod space; +mod text; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use pane_grid::PaneGrid; +#[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +pub use column::Column; +pub use image::Image; +pub use row::Row; +pub use space::Space; +pub use svg::Svg; +pub use text::Text; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; diff --git a/wgpu/src/renderer/widget/button.rs b/graphics/src/widget/button.rs index eb225038..2e3f78ca 100644 --- a/wgpu/src/renderer/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -1,9 +1,26 @@ -use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; use iced_native::{ - mouse, Background, Color, Element, Layout, Point, Rectangle, Vector, + Background, Color, Element, Layout, Point, Rectangle, Vector, }; -impl iced_native::button::Renderer for 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, Backend> = + iced_native::Button<'a, Message, Renderer<Backend>>; + +impl<B> iced_native::button::Renderer for Renderer<B> +where + B: Backend, +{ const DEFAULT_PADDING: u16 = 5; type Style = Box<dyn StyleSheet>; @@ -42,10 +59,11 @@ impl iced_native::button::Renderer for Renderer { }, content_layout, cursor_position, + &bounds, ); ( - if styling.background.is_some() || styling.border_width > 0 { + if styling.background.is_some() || styling.border_width > 0.0 { let background = Primitive::Quad { bounds, background: styling @@ -72,7 +90,7 @@ impl iced_native::button::Renderer for Renderer { [0.0, 0.0, 0.0, 0.5].into(), ), border_radius: styling.border_radius, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }; @@ -83,7 +101,7 @@ impl iced_native::button::Renderer for Renderer { } else { content }, - if is_mouse_over { + if is_mouse_over && !is_disabled { mouse::Interaction::Pointer } else { mouse::Interaction::default() diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs new file mode 100644 index 00000000..95ede50f --- /dev/null +++ b/graphics/src/widget/canvas.rs @@ -0,0 +1,236 @@ +//! 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! +use crate::{Backend, Defaults, Primitive, Renderer}; +use iced_native::layout; +use iced_native::mouse; +use iced_native::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, +}; +use std::hash::Hash; +use std::marker::PhantomData; + +pub mod event; +pub mod path; + +mod cache; +mod cursor; +mod fill; +mod frame; +mod geometry; +mod program; +mod stroke; +mod text; + +pub use cache::Cache; +pub use cursor::Cursor; +pub use event::Event; +pub use fill::{Fill, FillRule}; +pub use frame::Frame; +pub use geometry::Geometry; +pub use path::Path; +pub use program::Program; +pub use stroke::{LineCap, LineJoin, Stroke}; +pub use text::Text; + +/// A widget capable of drawing 2D graphics. +/// +/// # Examples +/// The repository has a couple of [examples] showcasing how to use a +/// [`Canvas`]: +/// +/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock +/// and its hands to display the current time. +/// - [`game_of_life`], an interactive version of the Game of Life, invented by +/// John Conway. +/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget +/// and showcasing how to compose different transforms. +/// +/// [examples]: https://github.com/hecrj/iced/tree/master/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life +/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub use iced_graphics::canvas; +/// # pub use iced_native::{Color, Rectangle}; +/// # } +/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas<Message, P: Program<Message>> { + width: Length, + height: Length, + program: P, + phantom: PhantomData<Message>, +} + +impl<Message, P: Program<Message>> Canvas<Message, P> { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + program, + phantom: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } +} + +impl<Message, P, B> Widget<Message, Renderer<B>> for Canvas<Message, P> +where + P: Program<Message>, + B: Backend, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer<B>, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + _renderer: &Renderer<B>, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + iced_native::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let (event_status, message) = + self.program.update(canvas_event, bounds, cursor); + + if let Some(message) = message { + messages.push(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn draw( + &self, + _renderer: &mut Renderer<B>, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> (Primitive, mouse::Interaction) { + let bounds = layout.bounds(); + let translation = Vector::new(bounds.x, bounds.y); + let cursor = Cursor::from_window_position(cursor_position); + + ( + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { + primitives: self + .program + .draw(bounds, cursor) + .into_iter() + .map(Geometry::into_primitive) + .collect(), + }), + }, + self.program.mouse_interaction(bounds, cursor), + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message, P, B> From<Canvas<Message, P>> + for Element<'a, Message, Renderer<B>> +where + Message: 'static, + P: Program<Message> + 'a, + B: Backend, +{ + fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer<B>> { + Element::new(canvas) + } +} diff --git a/wgpu/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs index 4b28d164..a469417d 100644 --- a/wgpu/src/widget/canvas/cache.rs +++ b/graphics/src/widget/canvas/cache.rs @@ -23,10 +23,6 @@ impl Default for State { /// /// A [`Cache`] will not redraw its geometry unless the dimensions of its layer /// change or it is explicitly cleared. -/// -/// [`Layer`]: ../trait.Layer.html -/// [`Cache`]: struct.Cache.html -/// [`Geometry`]: struct.Geometry.html #[derive(Debug, Default)] pub struct Cache { state: RefCell<State>, @@ -34,8 +30,6 @@ pub struct Cache { impl Cache { /// Creates a new empty [`Cache`]. - /// - /// [`Cache`]: struct.Cache.html pub fn new() -> Self { Cache { state: Default::default(), @@ -43,8 +37,6 @@ impl Cache { } /// Clears the [`Cache`], forcing a redraw the next time it is used. - /// - /// [`Cache`]: struct.Cache.html pub fn clear(&mut self) { *self.state.borrow_mut() = State::Empty; } @@ -59,8 +51,6 @@ impl Cache { /// Otherwise, the previously stored [`Geometry`] will be returned. The /// [`Cache`] is not cleared in this case. In other words, it will keep /// returning the stored [`Geometry`] if needed. - /// - /// [`Cache`]: struct.Cache.html pub fn draw(&self, bounds: Size, draw_fn: impl Fn(&mut Frame)) -> Geometry { use std::ops::Deref; diff --git a/wgpu/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs index 456760ea..9588d129 100644 --- a/wgpu/src/widget/canvas/cursor.rs +++ b/graphics/src/widget/canvas/cursor.rs @@ -22,8 +22,6 @@ impl Cursor { } /// Returns the absolute position of the [`Cursor`], if available. - /// - /// [`Cursor`]: enum.Cursor.html pub fn position(&self) -> Option<Point> { match self { Cursor::Available(position) => Some(*position), @@ -36,8 +34,6 @@ impl Cursor { /// /// If the [`Cursor`] is not over the provided bounds, this method will /// return `None`. - /// - /// [`Cursor`]: enum.Cursor.html pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> { if self.is_over(bounds) { self.position_from(bounds.position()) @@ -48,8 +44,6 @@ impl Cursor { /// Returns the relative position of the [`Cursor`] from the given origin, /// if available. - /// - /// [`Cursor`]: enum.Cursor.html pub fn position_from(&self, origin: Point) -> Option<Point> { match self { Cursor::Available(position) => { @@ -61,8 +55,6 @@ impl Cursor { /// Returns whether the [`Cursor`] is currently over the provided bounds /// or not. - /// - /// [`Cursor`]: enum.Cursor.html pub fn is_over(&self, bounds: &Rectangle) -> bool { match self { Cursor::Available(position) => bounds.contains(*position), diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs new file mode 100644 index 00000000..5bf6f7a6 --- /dev/null +++ b/graphics/src/widget/canvas/event.rs @@ -0,0 +1,17 @@ +//! Handle events of a canvas. +use iced_native::keyboard; +use iced_native::mouse; + +pub use iced_native::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs new file mode 100644 index 00000000..56495435 --- /dev/null +++ b/graphics/src/widget/canvas/fill.rs @@ -0,0 +1,60 @@ +use iced_native::Color; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub struct Fill { + /// The color used to fill geometry. + /// + /// By default, it is set to `BLACK`. + pub color: Color, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: FillRule, +} + +impl Default for Fill { + fn default() -> Fill { + Fill { + color: Color::BLACK, + rule: FillRule::NonZero, + } + } +} + +impl From<Color> for Fill { + fn from(color: Color) -> Fill { + Fill { + color, + ..Fill::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum FillRule { + NonZero, + EvenOdd, +} + +impl From<FillRule> for lyon::tessellation::FillRule { + fn from(rule: FillRule) -> lyon::tessellation::FillRule { + match rule { + FillRule::NonZero => lyon::tessellation::FillRule::NonZero, + FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 5262ab4e..b86f9e04 100644 --- a/wgpu/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -7,7 +7,7 @@ use crate::{ /// The frame of a [`Canvas`]. /// -/// [`Canvas`]: struct.Canvas.html +/// [`Canvas`]: crate::widget::Canvas #[derive(Debug)] pub struct Frame { size: Size, @@ -33,8 +33,6 @@ impl Frame { /// /// 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(size: Size) -> Frame { Frame { size, @@ -51,32 +49,24 @@ impl Frame { } /// Returns the width of the [`Frame`]. - /// - /// [`Frame`]: struct.Frame.html #[inline] pub fn width(&self) -> f32 { self.size.width } /// Returns the width of the [`Frame`]. - /// - /// [`Frame`]: struct.Frame.html #[inline] pub fn height(&self) -> f32 { self.size.height } /// Returns the dimensions of the [`Frame`]. - /// - /// [`Frame`]: struct.Frame.html #[inline] pub fn size(&self) -> Size { self.size } /// Returns the coordinate of the center of the [`Frame`]. - /// - /// [`Frame`]: struct.Frame.html #[inline] pub fn center(&self) -> Point { Point::new(self.size.width / 2.0, self.size.height / 2.0) @@ -84,37 +74,27 @@ impl Frame { /// 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: impl Into<Fill>) { use lyon::tessellation::{ BuffersBuilder, FillOptions, FillTessellator, }; + let Fill { color, rule } = fill.into(); + let mut buffers = BuffersBuilder::new( &mut self.buffers, - FillVertex(match fill.into() { - Fill::Color(color) => color.into_linear(), - }), + FillVertex(color.into_linear()), ); let mut tessellator = FillTessellator::new(); + let options = FillOptions::default().with_fill_rule(rule.into()); let result = if self.transforms.current.is_identity { - tessellator.tessellate_path( - path.raw(), - &FillOptions::default(), - &mut buffers, - ) + tessellator.tessellate_path(path.raw(), &options, &mut buffers) } else { let path = path.transformed(&self.transforms.current.raw); - tessellator.tessellate_path( - path.raw(), - &FillOptions::default(), - &mut buffers, - ) + tessellator.tessellate_path(path.raw(), &options, &mut buffers) }; let _ = result.expect("Tessellate path"); @@ -122,8 +102,6 @@ impl Frame { /// Draws an axis-aligned rectangle given its top-left corner coordinate and /// its `Size` on the [`Frame`] by filling it with the provided style. - /// - /// [`Frame`]: struct.Frame.html pub fn fill_rectangle( &mut self, top_left: Point, @@ -132,11 +110,11 @@ impl Frame { ) { use lyon::tessellation::{BuffersBuilder, FillOptions}; + let Fill { color, rule } = fill.into(); + let mut buffers = BuffersBuilder::new( &mut self.buffers, - FillVertex(match fill.into() { - Fill::Color(color) => color.into_linear(), - }), + FillVertex(color.into_linear()), ); let top_left = @@ -151,7 +129,7 @@ impl Frame { let _ = lyon::tessellation::basic_shapes::fill_rectangle( &lyon::math::Rect::new(top_left, size.into()), - &FillOptions::default(), + &FillOptions::default().with_fill_rule(rule.into()), &mut buffers, ) .expect("Fill rectangle"); @@ -159,9 +137,6 @@ impl Frame { /// 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: impl Into<Stroke>) { use lyon::tessellation::{ BuffersBuilder, StrokeOptions, StrokeTessellator, @@ -207,9 +182,7 @@ impl Frame { /// Support for vectorial text is planned, and should address all these /// limitations. /// - /// [`Text`]: struct.Text.html - /// [`Frame`]: struct.Frame.html - /// [`Canvas`]: struct.Canvas.html + /// [`Canvas`]: crate::widget::Canvas pub fn fill_text(&mut self, text: impl Into<Text>) { use std::f32; @@ -247,8 +220,6 @@ impl Frame { /// /// 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); @@ -259,8 +230,6 @@ impl Frame { } /// 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 @@ -275,21 +244,17 @@ impl Frame { } /// 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)); + .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 = @@ -298,17 +263,14 @@ impl Frame { } /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. - /// - /// [`Frame`]: struct.Frame.html - /// [`Geometry`]: struct.Geometry.html pub fn into_geometry(mut self) -> Geometry { if !self.buffers.indices.is_empty() { self.primitives.push(Primitive::Mesh2D { - size: self.size, buffers: triangle::Mesh2D { vertices: self.buffers.vertices, indices: self.buffers.indices, }, + size: self.size, }); } diff --git a/wgpu/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs index 4cadee39..8915cda1 100644 --- a/wgpu/src/widget/canvas/geometry.rs +++ b/graphics/src/widget/canvas/geometry.rs @@ -5,9 +5,8 @@ use crate::Primitive; /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a /// [`Cache`]. /// -/// [`Geometry`]: struct.Geometry.html -/// [`Frame`]: struct.Frame.html -/// [`Cache`]: struct.Cache.html +/// [`Frame`]: crate::widget::canvas::Frame +/// [`Cache`]: crate::widget::canvas::Cache #[derive(Debug, Clone)] pub struct Geometry(Primitive); @@ -19,9 +18,6 @@ impl Geometry { /// Turns the [`Geometry`] into a [`Primitive`]. /// /// This can be useful if you are building a custom widget. - /// - /// [`Geometry`]: struct.Geometry.html - /// [`Primitive`]: ../enum.Primitive.html pub fn into_primitive(self) -> Primitive { self.0 } diff --git a/wgpu/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs index c26bf187..6de19321 100644 --- a/wgpu/src/widget/canvas/path.rs +++ b/graphics/src/widget/canvas/path.rs @@ -12,8 +12,6 @@ use iced_native::{Point, Size}; /// 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, @@ -23,9 +21,6 @@ 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(); @@ -37,8 +32,6 @@ impl Path { /// Creates a new [`Path`] representing a line segment given its starting /// and end points. - /// - /// [`Path`]: struct.Path.html pub fn line(from: Point, to: Point) -> Self { Self::new(|p| { p.move_to(from); @@ -48,16 +41,12 @@ impl Path { /// Creates a new [`Path`] representing a rectangle given its top-left /// corner coordinate and its `Size`. - /// - /// [`Path`]: struct.Path.html pub fn rectangle(top_left: Point, size: Size) -> Self { Self::new(|p| p.rectangle(top_left, size)) } /// Creates a new [`Path`] representing a circle given its center /// coordinate and its radius. - /// - /// [`Path`]: struct.Path.html pub fn circle(center: Point, radius: f32) -> Self { Self::new(|p| p.circle(center, radius)) } diff --git a/wgpu/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs index 343191f1..b8e72daf 100644 --- a/wgpu/src/widget/canvas/path/arc.rs +++ b/graphics/src/widget/canvas/path/arc.rs @@ -15,8 +15,6 @@ pub struct Arc { } /// An elliptical [`Arc`]. -/// -/// [`Arc`]: struct.Arc.html #[derive(Debug, Clone, Copy)] pub struct Elliptical { /// The center of the arc. diff --git a/wgpu/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs index 6511fa52..5ce0e02c 100644 --- a/wgpu/src/widget/canvas/path/builder.rs +++ b/graphics/src/widget/canvas/path/builder.rs @@ -6,8 +6,6 @@ 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>, @@ -15,8 +13,6 @@ pub struct Builder { impl Builder { /// Creates a new [`Builder`]. - /// - /// [`Builder`]: struct.Builder.html pub fn new() -> Builder { Builder { raw: lyon::path::Path::builder().with_svg(), @@ -31,8 +27,6 @@ impl Builder { /// 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)); @@ -40,9 +34,6 @@ impl Builder { /// 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()); @@ -53,8 +44,6 @@ impl Builder { /// /// 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}; @@ -72,10 +61,7 @@ impl Builder { ); } - /// Adds an [`Ellipse`] to the [`Path`] using a clockwise direction. - /// - /// [`Ellipse`]: struct.Arc.html - /// [`Path`]: struct.Path.html + /// Adds an ellipse to the [`Path`] using a clockwise direction. pub fn ellipse(&mut self, arc: arc::Elliptical) { use lyon::{geom, math}; @@ -84,7 +70,7 @@ impl Builder { 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), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), }; let _ = self.raw.move_to(arc.sample(0.0)); @@ -96,8 +82,6 @@ impl Builder { /// 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, @@ -116,8 +100,6 @@ impl Builder { /// 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; @@ -130,8 +112,6 @@ impl Builder { /// 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, top_left: Point, size: Size) { self.move_to(top_left); @@ -146,8 +126,6 @@ impl Builder { /// 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 { @@ -160,17 +138,12 @@ impl Builder { /// 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 { diff --git a/wgpu/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index 725d9d72..d703caad 100644 --- a/wgpu/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -1,4 +1,5 @@ -use crate::canvas::{Cursor, Event, Geometry}; +use crate::canvas::event::{self, Event}; +use crate::canvas::{Cursor, Geometry}; use iced_native::{mouse, Rectangle}; /// The state and logic of a [`Canvas`]. @@ -6,8 +7,7 @@ use iced_native::{mouse, Rectangle}; /// A [`Program`] can mutate internal state and produce messages for an /// application. /// -/// [`Canvas`]: struct.Canvas.html -/// [`Program`]: trait.Program.html +/// [`Canvas`]: crate::widget::Canvas pub trait Program<Message> { /// Updates the state of the [`Program`]. /// @@ -19,16 +19,14 @@ pub trait Program<Message> { /// /// By default, this method does and returns nothing. /// - /// [`Program`]: trait.Program.html - /// [`Canvas`]: struct.Canvas.html - /// [`Event`]: enum.Event.html + /// [`Canvas`]: crate::widget::Canvas fn update( &mut self, _event: Event, _bounds: Rectangle, _cursor: Cursor, - ) -> Option<Message> { - None + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) } /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. @@ -36,10 +34,8 @@ pub trait Program<Message> { /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a /// [`Cache`]. /// - /// [`Program`]: trait.Program.html - /// [`Geometry`]: struct.Geometry.html - /// [`Frame`]: struct.Frame.html - /// [`Cache`]: struct.Cache.html + /// [`Frame`]: crate::widget::canvas::Cache + /// [`Cache`]: crate::widget::canvas::Cache fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>; /// Returns the current mouse interaction of the [`Program`]. @@ -47,8 +43,7 @@ pub trait Program<Message> { /// The interaction returned will be in effect even if the cursor position /// is out of bounds of the program's [`Canvas`]. /// - /// [`Program`]: trait.Program.html - /// [`Canvas`]: struct.Canvas.html + /// [`Canvas`]: crate::widget::Canvas fn mouse_interaction( &self, _bounds: Rectangle, @@ -67,7 +62,7 @@ where event: Event, bounds: Rectangle, cursor: Cursor, - ) -> Option<Message> { + ) -> (event::Status, Option<Message>) { T::update(self, event, bounds, cursor) } diff --git a/wgpu/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs index 5b6fc56a..9f0449d0 100644 --- a/wgpu/src/widget/canvas/stroke.rs +++ b/graphics/src/widget/canvas/stroke.rs @@ -16,31 +16,21 @@ pub struct Stroke { impl Stroke { /// Sets the color of the [`Stroke`]. - /// - /// [`Stroke`]: struct.Stroke.html pub fn with_color(self, color: Color) -> Stroke { Stroke { color, ..self } } /// Sets the width of the [`Stroke`]. - /// - /// [`Stroke`]: struct.Stroke.html pub fn with_width(self, width: f32) -> Stroke { Stroke { width, ..self } } /// Sets the [`LineCap`] of the [`Stroke`]. - /// - /// [`LineCap`]: enum.LineCap.html - /// [`Stroke`]: struct.Stroke.html pub fn with_line_cap(self, line_cap: LineCap) -> Stroke { Stroke { line_cap, ..self } } /// Sets the [`LineJoin`] of the [`Stroke`]. - /// - /// [`LineJoin`]: enum.LineJoin.html - /// [`Stroke`]: struct.Stroke.html pub fn with_line_join(self, line_join: LineJoin) -> Stroke { Stroke { line_join, ..self } } diff --git a/wgpu/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs index c4cae30e..c4cae30e 100644 --- a/wgpu/src/widget/canvas/text.rs +++ b/graphics/src/widget/canvas/text.rs diff --git a/wgpu/src/renderer/widget/checkbox.rs b/graphics/src/widget/checkbox.rs index 0340bf62..cb7fd2cf 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/graphics/src/widget/checkbox.rs @@ -1,9 +1,22 @@ -use crate::{checkbox::StyleSheet, Primitive, Renderer}; -use iced_native::{ - checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment, -}; +//! Show toggle controls using checkboxes. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::checkbox; +use iced_native::mouse; +use iced_native::{HorizontalAlignment, Rectangle, VerticalAlignment}; -impl checkbox::Renderer for 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, Backend> = + iced_native::Checkbox<Message, Renderer<Backend>>; + +impl<B> checkbox::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ type Style = Box<dyn StyleSheet>; const DEFAULT_SIZE: u16 = 20; @@ -35,8 +48,8 @@ impl checkbox::Renderer for Renderer { Primitive::Group { primitives: if is_checked { let check = Primitive::Text { - content: crate::text::CHECKMARK_ICON.to_string(), - font: crate::text::BUILTIN_ICONS, + content: B::CHECKMARK_ICON.to_string(), + font: B::ICON_FONT, size: bounds.height * 0.7, bounds: Rectangle { x: bounds.center_x(), diff --git a/wgpu/src/renderer/widget/row.rs b/graphics/src/widget/column.rs index d0b7ef09..0cf56842 100644 --- a/wgpu/src/renderer/widget/row.rs +++ b/graphics/src/widget/column.rs @@ -1,24 +1,39 @@ -use crate::{Primitive, Renderer}; -use iced_native::{mouse, row, Element, Layout, Point}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::column; +use iced_native::mouse; +use iced_native::{Element, Layout, Point, Rectangle}; -impl row::Renderer for Renderer { +/// A container that distributes its contents vertically. +pub type Column<'a, Message, Backend> = + iced_native::Column<'a, Message, Renderer<Backend>>; + +impl<B> column::Renderer for Renderer<B> +where + B: Backend, +{ fn draw<Message>( &mut self, defaults: &Self::Defaults, - children: &[Element<'_, Message, Self>], + content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output { let mut mouse_interaction = mouse::Interaction::default(); ( Primitive::Group { - primitives: children + primitives: content .iter() .zip(layout.children()) .map(|(child, layout)| { - let (primitive, new_mouse_interaction) = - child.draw(self, defaults, layout, cursor_position); + let (primitive, new_mouse_interaction) = child.draw( + self, + defaults, + layout, + cursor_position, + viewport, + ); if new_mouse_interaction > mouse_interaction { mouse_interaction = new_mouse_interaction; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs new file mode 100644 index 00000000..aae3e1d8 --- /dev/null +++ b/graphics/src/widget/container.rs @@ -0,0 +1,78 @@ +//! Decorate content and apply alignment. +use crate::container; +use crate::defaults::{self, Defaults}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message, Backend> = + iced_native::Container<'a, Message, Renderer<Backend>>; + +impl<B> iced_native::container::Renderer for Renderer<B> +where + B: Backend, +{ + type Style = Box<dyn container::StyleSheet>; + + fn draw<Message>( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + viewport: &Rectangle, + 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), + }, + }; + + let (content, mouse_interaction) = content.draw( + self, + &defaults, + content_layout, + cursor_position, + viewport, + ); + + if let Some(background) = background(bounds, &style) { + ( + Primitive::Group { + primitives: vec![background, content], + }, + mouse_interaction, + ) + } else { + (content, mouse_interaction) + } + } +} + +pub(crate) fn background( + bounds: Rectangle, + style: &container::Style, +) -> Option<Primitive> { + if style.background.is_some() || style.border_width > 0.0 { + Some(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, + }) + } else { + None + } +} diff --git a/wgpu/src/renderer/widget/image.rs b/graphics/src/widget/image.rs index d32c078a..bdf03de3 100644 --- a/wgpu/src/renderer/widget/image.rs +++ b/graphics/src/widget/image.rs @@ -1,11 +1,21 @@ -mod viewer; +//! Display images in your user interface. +pub mod viewer; + +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; -use iced_native::{image, mouse, Layout}; +use iced_native::image; +use iced_native::mouse; +use iced_native::Layout; + +pub use iced_native::image::{Handle, Image, Viewer}; -impl image::Renderer for Renderer { +impl<B> image::Renderer for Renderer<B> +where + B: Backend + backend::Image, +{ fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { - self.image_pipeline.dimensions(handle) + self.backend().dimensions(handle) } fn draw( diff --git a/wgpu/src/renderer/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs index 2599bfa5..b6217ff7 100644 --- a/wgpu/src/renderer/widget/image/viewer.rs +++ b/graphics/src/widget/image/viewer.rs @@ -1,10 +1,16 @@ +//! Zoom and pan on an image. +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; + use iced_native::{ image::{self, viewer}, mouse, Rectangle, Vector, }; -impl viewer::Renderer for Renderer { +impl<B> viewer::Renderer for Renderer<B> +where + B: Backend + backend::Image, +{ fn draw( &mut self, state: &viewer::State, diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs new file mode 100644 index 00000000..f09984fc --- /dev/null +++ b/graphics/src/widget/pane_grid.rs @@ -0,0 +1,252 @@ +//! Let your users split regions of your application and organize layout dynamically. +//! +//! [](https://gfycat.com/mixedflatjellyfish) +//! +//! # Example +//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, +//! drag and drop, and hotkey support. +//! +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid +use crate::backend::{self, Backend}; +use crate::defaults; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::pane_grid; +use iced_native::text; +use iced_native::{ + Element, HorizontalAlignment, Layout, Point, Rectangle, Vector, + VerticalAlignment, +}; + +pub use iced_native::pane_grid::{ + Axis, Configuration, Content, Direction, DragEvent, Pane, ResizeEvent, + Split, State, TitleBar, +}; + +/// A collection of panes distributed using either vertical or horizontal splits +/// to completely fill the space available. +/// +/// [](https://gfycat.com/mixedflatjellyfish) +/// +/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. +pub type PaneGrid<'a, Message, Backend> = + iced_native::PaneGrid<'a, Message, Renderer<Backend>>; + +impl<B> pane_grid::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + fn draw<Message>( + &mut self, + defaults: &Self::Defaults, + content: &[(Pane, Content<'_, Message, Self>)], + dragging: Option<(Pane, Point)>, + resizing: Option<Axis>, + layout: Layout<'_>, + cursor_position: Point, + ) -> Self::Output { + let pane_cursor_position = if dragging.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let mut mouse_interaction = mouse::Interaction::default(); + let mut dragged_pane = None; + + let mut panes: Vec<_> = content + .iter() + .zip(layout.children()) + .enumerate() + .map(|(i, ((id, pane), layout))| { + let (primitive, new_mouse_interaction) = + pane.draw(self, defaults, layout, pane_cursor_position); + + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; + } + + if let Some((dragging, origin)) = dragging { + if *id == dragging { + dragged_pane = Some((i, layout, origin)); + } + } + + primitive + }) + .collect(); + + let primitives = if let Some((index, layout, origin)) = dragged_pane { + let pane = panes.remove(index); + let bounds = layout.bounds(); + + // TODO: Fix once proper layering is implemented. + // This is a pretty hacky way to achieve layering. + let clip = Primitive::Clip { + bounds: Rectangle { + x: cursor_position.x - origin.x, + y: cursor_position.y - origin.y, + width: bounds.width + 0.5, + height: bounds.height + 0.5, + }, + offset: Vector::new(0, 0), + content: Box::new(Primitive::Translate { + translation: Vector::new( + cursor_position.x - bounds.x - origin.x, + cursor_position.y - bounds.y - origin.y, + ), + content: Box::new(pane), + }), + }; + + panes.push(clip); + + panes + } else { + panes + }; + + ( + Primitive::Group { primitives }, + if dragging.is_some() { + mouse::Interaction::Grabbing + } else if let Some(axis) = resizing { + match axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + } + } else { + mouse_interaction + }, + ) + } + + fn draw_pane<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style_sheet: &Self::Style, + title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, + body: (&Element<'_, Message, Self>, Layout<'_>), + cursor_position: Point, + ) -> Self::Output { + let style = style_sheet.style(); + let (body, body_layout) = body; + + let (body_primitive, body_interaction) = + body.draw(self, defaults, body_layout, cursor_position, &bounds); + + let background = crate::widget::container::background(bounds, &style); + + if let Some((title_bar, title_bar_layout)) = title_bar { + let show_controls = bounds.contains(cursor_position); + let is_over_pick_area = + title_bar.is_over_pick_area(title_bar_layout, cursor_position); + + let (title_bar_primitive, title_bar_interaction) = title_bar.draw( + self, + defaults, + title_bar_layout, + cursor_position, + show_controls, + ); + + ( + Primitive::Group { + primitives: vec![ + background.unwrap_or(Primitive::None), + title_bar_primitive, + body_primitive, + ], + }, + if is_over_pick_area { + mouse::Interaction::Grab + } else if title_bar_interaction > body_interaction { + title_bar_interaction + } else { + body_interaction + }, + ) + } else { + ( + if let Some(background) = background { + Primitive::Group { + primitives: vec![background, body_primitive], + } + } else { + body_primitive + }, + body_interaction, + ) + } + } + + fn draw_title_bar<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style_sheet: &Self::Style, + title: &str, + title_size: u16, + title_font: Self::Font, + title_bounds: Rectangle, + controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, + cursor_position: Point, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Self::Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + }; + + let background = crate::widget::container::background(bounds, &style); + + let (title_primitive, _) = text::Renderer::draw( + self, + &defaults, + title_bounds, + title, + title_size, + title_font, + None, + HorizontalAlignment::Left, + VerticalAlignment::Top, + ); + + if let Some((controls, controls_layout)) = controls { + let (controls_primitive, controls_interaction) = controls.draw( + self, + &defaults, + controls_layout, + cursor_position, + &bounds, + ); + + ( + Primitive::Group { + primitives: vec![ + background.unwrap_or(Primitive::None), + title_primitive, + controls_primitive, + ], + }, + controls_interaction, + ) + } else { + ( + if let Some(background) = background { + Primitive::Group { + primitives: vec![background, title_primitive], + } + } else { + title_primitive + }, + mouse::Interaction::default(), + ) + } + } +} diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs new file mode 100644 index 00000000..f42a8707 --- /dev/null +++ b/graphics/src/widget/pick_list.rs @@ -0,0 +1,97 @@ +//! Display a dropdown list of selectable values. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::{ + mouse, Font, HorizontalAlignment, Point, Rectangle, VerticalAlignment, +}; +use iced_style::menu; + +pub use iced_native::pick_list::State; +pub use iced_style::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message, Backend> = + iced_native::PickList<'a, T, Message, Renderer<Backend>>; + +impl<B> iced_native::pick_list::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Box<dyn StyleSheet>; + + const DEFAULT_PADDING: u16 = 5; + + fn menu_style(style: &Box<dyn StyleSheet>) -> menu::Style { + style.menu() + } + + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Font, + style: &Box<dyn StyleSheet>, + ) -> Self::Output { + let is_mouse_over = bounds.contains(cursor_position); + + let style = if is_mouse_over { + style.hovered() + } else { + style.active() + }; + + let background = Primitive::Quad { + bounds, + background: style.background, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }; + + let arrow_down = Primitive::Text { + content: B::ARROW_DOWN_ICON.to_string(), + font: B::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding) * 2.0, + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Center, + }; + + ( + Primitive::Group { + primitives: if let Some(label) = selected { + let label = Primitive::Text { + content: label, + size: f32::from(text_size), + font, + color: style.text_color, + bounds: Rectangle { + x: bounds.x + f32::from(padding), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: HorizontalAlignment::Left, + vertical_alignment: VerticalAlignment::Center, + }; + + vec![background, label, arrow_down] + } else { + vec![background, arrow_down] + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs index 2baeeb14..32ee42c6 100644 --- a/wgpu/src/renderer/widget/progress_bar.rs +++ b/graphics/src/widget/progress_bar.rs @@ -1,7 +1,24 @@ -use crate::{progress_bar::StyleSheet, Primitive, Renderer}; -use iced_native::{mouse, progress_bar, Color, Rectangle}; +//! Allow your users to visually track the progress of a computation. +//! +//! A [`ProgressBar`] has a range of possible values and a current value, +//! as well as a length, height and style. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::progress_bar; +use iced_native::{Color, Rectangle}; -impl progress_bar::Renderer for 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<Backend> = iced_native::ProgressBar<Renderer<Backend>>; + +impl<B> progress_bar::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; const DEFAULT_HEIGHT: u16 = 30; @@ -14,17 +31,20 @@ impl progress_bar::Renderer for Renderer { 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 active_progress_width = if range_start >= range_end { + 0.0 + } else { + bounds.width * (value - range_start) / (range_end - range_start) + }; let background = Primitive::Group { primitives: vec![Primitive::Quad { bounds: Rectangle { ..bounds }, background: style.background, border_radius: style.border_radius, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }], }; @@ -38,7 +58,7 @@ impl progress_bar::Renderer for Renderer { }, background: style.bar, border_radius: style.border_radius, - border_width: 0, + border_width: 0.0, border_color: Color::TRANSPARENT, }; diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs new file mode 100644 index 00000000..b3a01dd7 --- /dev/null +++ b/graphics/src/widget/qr_code.rs @@ -0,0 +1,305 @@ +//! Encode and display information in a QR code. +use crate::canvas; +use crate::{Backend, Defaults, Primitive, Renderer, Vector}; + +use iced_native::{ + layout, mouse, Color, Element, Hasher, Layout, Length, Point, Rectangle, + Size, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, B> Widget<Message, Renderer<B>> for QRCode<'a> +where + B: Backend, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer<B>, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new( + f32::from(side_length), + f32::from(side_length), + )) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.state.contents.hash(state); + } + + fn draw( + &self, + _renderer: &mut Renderer<B>, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> (Primitive, mouse::Interaction) { + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = self.state.cache.draw(bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32)); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + ( + Primitive::Translate { + translation: Vector::new(bounds.x, bounds.y), + content: Box::new(geometry.into_primitive()), + }, + mouse::Interaction::default(), + ) + } +} + +impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for QRCode<'a> +where + B: Backend, +{ + fn into(self) -> Element<'a, Message, Renderer<B>> { + Element::new(self) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec<qrcode::Color>, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From<Version> for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From<ErrorCorrection> for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From<qrcode::types::QrError> for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/wgpu/src/renderer/widget/radio.rs b/graphics/src/widget/radio.rs index cee0deb6..fd3d8145 100644 --- a/wgpu/src/renderer/widget/radio.rs +++ b/graphics/src/widget/radio.rs @@ -1,13 +1,25 @@ -use crate::{radio::StyleSheet, Primitive, Renderer}; -use iced_native::{mouse, radio, Background, Color, Rectangle}; +//! Create choices using radio buttons. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::radio; +use iced_native::{Background, Color, Rectangle}; -const SIZE: f32 = 28.0; -const DOT_SIZE: f32 = SIZE / 2.0; +pub use iced_style::radio::{Style, StyleSheet}; -impl radio::Renderer for Renderer { +/// 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, Backend> = + iced_native::Radio<Message, Renderer<Backend>>; + +impl<B> radio::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; - const DEFAULT_SIZE: u16 = SIZE as u16; + const DEFAULT_SIZE: u16 = 28; const DEFAULT_SPACING: u16 = 15; fn draw( @@ -24,10 +36,13 @@ impl radio::Renderer for Renderer { style_sheet.active() }; + let size = bounds.width; + let dot_size = size / 2.0; + let radio = Primitive::Quad { bounds, background: style.background, - border_radius: (SIZE / 2.0) as u16, + border_radius: size / 2.0, border_width: style.border_width, border_color: style.border_color, }; @@ -37,14 +52,14 @@ impl radio::Renderer for Renderer { primitives: if is_selected { let radio_circle = Primitive::Quad { bounds: Rectangle { - x: bounds.x + DOT_SIZE / 2.0, - y: bounds.y + DOT_SIZE / 2.0, - width: bounds.width - DOT_SIZE, - height: bounds.height - DOT_SIZE, + x: bounds.x + dot_size / 2.0, + y: bounds.y + dot_size / 2.0, + width: bounds.width - dot_size, + height: bounds.height - dot_size, }, background: Background::Color(style.dot_color), - border_radius: (DOT_SIZE / 2.0) as u16, - border_width: 0, + border_radius: dot_size / 2.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }; diff --git a/wgpu/src/renderer/widget/column.rs b/graphics/src/widget/row.rs index b853276d..397d80bf 100644 --- a/wgpu/src/renderer/widget/column.rs +++ b/graphics/src/widget/row.rs @@ -1,13 +1,23 @@ -use crate::{Primitive, Renderer}; -use iced_native::{column, mouse, Element, Layout, Point}; +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::row; +use iced_native::{Element, Layout, Point, Rectangle}; -impl column::Renderer for Renderer { +/// A container that distributes its contents horizontally. +pub type Row<'a, Message, Backend> = + iced_native::Row<'a, Message, Renderer<Backend>>; + +impl<B> row::Renderer for Renderer<B> +where + B: Backend, +{ fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output { let mut mouse_interaction = mouse::Interaction::default(); @@ -17,8 +27,13 @@ impl column::Renderer for Renderer { .iter() .zip(layout.children()) .map(|(child, layout)| { - let (primitive, new_mouse_interaction) = - child.draw(self, defaults, layout, cursor_position); + let (primitive, new_mouse_interaction) = child.draw( + self, + defaults, + layout, + cursor_position, + viewport, + ); if new_mouse_interaction > mouse_interaction { mouse_interaction = new_mouse_interaction; diff --git a/graphics/src/widget/rule.rs b/graphics/src/widget/rule.rs new file mode 100644 index 00000000..835ebed8 --- /dev/null +++ b/graphics/src/widget/rule.rs @@ -0,0 +1,73 @@ +//! Display a horizontal or vertical rule for dividing content. + +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::rule; +use iced_native::{Background, Color, Rectangle}; + +pub use iced_style::rule::{FillMode, Style, StyleSheet}; + +/// Display a horizontal or vertical rule for dividing content. +/// +/// This is an alias of an `iced_native` rule with an `iced_graphics::Renderer`. +pub type Rule<Backend> = iced_native::Rule<Renderer<Backend>>; + +impl<B> rule::Renderer for Renderer<B> +where + B: Backend, +{ + type Style = Box<dyn StyleSheet>; + + fn draw( + &mut self, + bounds: Rectangle, + style_sheet: &Self::Style, + is_horizontal: bool, + ) -> Self::Output { + let style = style_sheet.style(); + + let line = if is_horizontal { + let line_y = (bounds.y + (bounds.height / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_width) = style.fill_mode.fill(bounds.width); + let line_x = bounds.x + offset; + + Primitive::Quad { + bounds: Rectangle { + x: line_x, + y: line_y, + width: line_width, + height: style.width as f32, + }, + background: Background::Color(style.color), + border_radius: style.radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } else { + let line_x = (bounds.x + (bounds.width / 2.0) + - (style.width as f32 / 2.0)) + .round(); + + let (offset, line_height) = style.fill_mode.fill(bounds.height); + let line_y = bounds.y + offset; + + Primitive::Quad { + bounds: Rectangle { + x: line_x, + y: line_y, + width: style.width as f32, + height: line_height, + }, + background: Background::Color(style.color), + border_radius: style.radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + }; + + (line, mouse::Interaction::default()) + } +} diff --git a/wgpu/src/renderer/widget/scrollable.rs b/graphics/src/widget/scrollable.rs index 8a400b82..57065ba2 100644 --- a/wgpu/src/renderer/widget/scrollable.rs +++ b/graphics/src/widget/scrollable.rs @@ -1,10 +1,24 @@ -use crate::{Primitive, Renderer}; -use iced_native::{mouse, scrollable, Background, Color, Rectangle, Vector}; +//! Navigate an endless amount of content with a scrollbar. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::scrollable; +use iced_native::{Background, Color, Rectangle, Vector}; -const SCROLLBAR_WIDTH: u16 = 10; -const SCROLLBAR_MARGIN: u16 = 2; +pub use iced_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; -impl scrollable::Renderer for Renderer { +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message, Backend> = + iced_native::Scrollable<'a, Message, Renderer<Backend>>; + +impl<B> scrollable::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn iced_style::scrollable::StyleSheet>; fn scrollbar( @@ -12,29 +26,45 @@ impl scrollable::Renderer for Renderer { bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<scrollable::Scrollbar> { if content_bounds.height > bounds.height { + let outer_width = + scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + + let outer_bounds = Rectangle { + x: bounds.x + bounds.width - outer_width as f32, + y: bounds.y, + width: outer_width as f32, + height: bounds.height, + }; + let scrollbar_bounds = Rectangle { x: bounds.x + bounds.width - - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + - f32::from(outer_width / 2 + scrollbar_width / 2), y: bounds.y, - width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + width: scrollbar_width as f32, height: bounds.height, }; let ratio = bounds.height / content_bounds.height; - let scrollbar_height = bounds.height * ratio; + let scroller_height = bounds.height * ratio; let y_offset = offset as f32 * ratio; let scroller_bounds = Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scroller_width / 2), y: scrollbar_bounds.y + y_offset, - width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), - height: scrollbar_height, + width: scroller_width as f32, + height: scroller_height, }; Some(scrollable::Scrollbar { + outer_bounds, bounds: scrollbar_bounds, + margin: scrollbar_margin, scroller: scrollable::Scroller { bounds: scroller_bounds, }, @@ -73,7 +103,7 @@ impl scrollable::Renderer for Renderer { }; let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0; + style.background.is_some() || style.border_width > 0.0; let scroller = if is_mouse_over || state.is_scroller_grabbed() @@ -92,12 +122,7 @@ impl scrollable::Renderer for Renderer { let scrollbar = if is_scrollbar_visible { Primitive::Quad { - bounds: Rectangle { - x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), - width: scrollbar.bounds.width - - f32::from(2 * SCROLLBAR_MARGIN), - ..scrollbar.bounds - }, + bounds: scrollbar.bounds, background: style .background .unwrap_or(Background::Color(Color::TRANSPARENT)), diff --git a/wgpu/src/renderer/widget/slider.rs b/graphics/src/widget/slider.rs index 220feace..aeceec3f 100644 --- a/wgpu/src/renderer/widget/slider.rs +++ b/graphics/src/widget/slider.rs @@ -1,17 +1,28 @@ -use crate::{ - slider::{HandleShape, StyleSheet}, - Primitive, Renderer, -}; -use iced_native::{mouse, slider, Background, Color, Point, Rectangle}; +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::slider; +use iced_native::{Background, Color, Point, Rectangle}; -const HANDLE_HEIGHT: f32 = 22.0; +pub use iced_native::slider::State; +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; -impl slider::Renderer for Renderer { +/// 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, T, Message, Backend> = + iced_native::Slider<'a, T, Message, Renderer<Backend>>; + +impl<B> slider::Renderer for Renderer<B> +where + B: Backend, +{ type Style = Box<dyn StyleSheet>; - fn height(&self) -> u32 { - 30 - } + const DEFAULT_HEIGHT: u16 = 22; fn draw( &mut self, @@ -43,8 +54,8 @@ impl slider::Renderer for Renderer { height: 2.0, }, background: Background::Color(style.rail_colors.0), - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, Primitive::Quad { @@ -55,27 +66,33 @@ impl slider::Renderer for Renderer { height: 2.0, }, background: Background::Color(style.rail_colors.1), - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, ); - let (range_start, range_end) = range.into_inner(); + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), f32::from(bounds.height), border_radius), + }; - let (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 (range_start, range_end) = range.into_inner(); - let handle_offset = (bounds.width - handle_width) - * ((value - range_start) / (range_end - range_start).max(1.0)); + let handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; let handle = Primitive::Quad { bounds: Rectangle { diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs new file mode 100644 index 00000000..1f31eabe --- /dev/null +++ b/graphics/src/widget/space.rs @@ -0,0 +1,15 @@ +use crate::{Backend, Primitive, Renderer}; +use iced_native::mouse; +use iced_native::space; +use iced_native::Rectangle; + +pub use iced_native::Space; + +impl<B> space::Renderer for Renderer<B> +where + B: Backend, +{ + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, mouse::Interaction::default()) + } +} diff --git a/wgpu/src/renderer/widget/svg.rs b/graphics/src/widget/svg.rs index f6d6d0ba..8b5ed66a 100644 --- a/wgpu/src/renderer/widget/svg.rs +++ b/graphics/src/widget/svg.rs @@ -1,9 +1,16 @@ +//! Display vector graphics in your application. +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::{mouse, svg, Layout}; -impl svg::Renderer for Renderer { +pub use iced_native::svg::{Handle, Svg}; + +impl<B> svg::Renderer for Renderer<B> +where + B: Backend + backend::Svg, +{ fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { - self.image_pipeline.viewport_dimensions(handle) + self.backend().viewport_dimensions(handle) } fn draw( diff --git a/wgpu/src/renderer/widget/text.rs b/graphics/src/widget/text.rs index 4605ed06..7e22e680 100644 --- a/wgpu/src/renderer/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -1,15 +1,28 @@ +//! Write some text for your users to read. +use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text; use iced_native::{ - mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size, - VerticalAlignment, + Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment, }; +/// A paragraph of text. +/// +/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. +pub type Text<Backend> = iced_native::Text<Renderer<Backend>>; + use std::f32; -impl text::Renderer for Renderer { +impl<B> text::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ type Font = Font; - const DEFAULT_SIZE: u16 = 20; + fn default_size(&self) -> u16 { + self.backend().default_size() + } fn measure( &self, @@ -18,7 +31,7 @@ impl text::Renderer for Renderer { font: Font, bounds: Size, ) -> (f32, f32) { - self.text_pipeline + self.backend() .measure(content, f32::from(size), font, bounds) } diff --git a/wgpu/src/renderer/widget/text_input.rs b/graphics/src/widget/text_input.rs index 57be6692..c269022b 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/graphics/src/widget/text_input.rs @@ -1,35 +1,36 @@ -use crate::{text_input::StyleSheet, Primitive, Renderer}; - +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::text_input::{self, cursor}; use iced_native::{ - mouse, - text_input::{self, cursor}, Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, Vector, VerticalAlignment, }; use std::f32; -impl text_input::Renderer for Renderer { - type Style = Box<dyn StyleSheet>; +pub use iced_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; - fn default_size(&self) -> u16 { - // TODO: Make this configurable - 20 - } +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message, Backend> = + iced_native::TextInput<'a, Message, Renderer<Backend>>; - 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, - ); +impl<B> text_input::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Box<dyn StyleSheet>; - let spaces_around = value.len() - value.trim().len(); + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let backend = self.backend(); - if spaces_around > 0 { - let space_width = self.text_pipeline.space_width(size as f32); - width += spaces_around as f32 * space_width; - } + let (width, _) = + backend.measure(value, f32::from(size), font, Size::INFINITY); width } @@ -145,8 +146,8 @@ impl text_input::Renderer for Renderer { background: Background::Color( style_sheet.value_color(), ), - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, offset, @@ -189,8 +190,8 @@ impl text_input::Renderer for Renderer { background: Background::Color( style_sheet.selection_color(), ), - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, if end == right { @@ -241,14 +242,17 @@ impl text_input::Renderer for Renderer { } } -fn measure_cursor_and_scroll_offset( - renderer: &Renderer, +fn measure_cursor_and_scroll_offset<B>( + renderer: &Renderer<B>, text_bounds: Rectangle, value: &text_input::Value, size: u16, cursor_index: usize, font: Font, -) -> (f32, f32) { +) -> (f32, f32) +where + B: Backend + backend::Text, +{ use iced_native::text_input::Renderer; let text_before_cursor = value.until(cursor_index).to_string(); diff --git a/graphics/src/window.rs b/graphics/src/window.rs new file mode 100644 index 00000000..3e74db5f --- /dev/null +++ b/graphics/src/window.rs @@ -0,0 +1,10 @@ +//! Draw graphics to window surfaces. +mod compositor; + +#[cfg(feature = "opengl")] +mod gl_compositor; + +pub use compositor::Compositor; + +#[cfg(feature = "opengl")] +pub use gl_compositor::GLCompositor; diff --git a/native/src/window/backend.rs b/graphics/src/window/compositor.rs index 892d4bb9..0bc8cbc8 100644 --- a/native/src/window/backend.rs +++ b/graphics/src/window/compositor.rs @@ -1,14 +1,14 @@ -use crate::mouse; - +use crate::{Color, Error, Viewport}; +use iced_native::mouse; use raw_window_handle::HasRawWindowHandle; -/// A graphics backend that can render to windows. -pub trait Backend: Sized { +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { /// The settings of the backend. type Settings: Default; /// The iced renderer of the backend. - type Renderer: crate::Renderer; + type Renderer: iced_native::Renderer; /// The surface of the backend. type Surface; @@ -16,14 +16,12 @@ pub trait Backend: Sized { /// 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); + /// Creates a new [`Compositor`]. + fn new(settings: Self::Settings) -> Result<(Self, Self::Renderer), Error>; /// Crates a new [`Surface`] for the given window. /// - /// [`Surface`]: #associatedtype.Surface + /// [`Surface`]: Self::Surface fn create_surface<W: HasRawWindowHandle>( &mut self, window: &W, @@ -31,8 +29,8 @@ pub trait Backend: Sized { /// Crates a new [`SwapChain`] for the given [`Surface`]. /// - /// [`SwapChain`]: #associatedtype.SwapChain - /// [`Surface`]: #associatedtype.Surface + /// [`SwapChain`]: Self::SwapChain + /// [`Surface`]: Self::Surface fn create_swap_chain( &mut self, surface: &Self::Surface, @@ -42,14 +40,14 @@ pub trait Backend: Sized { /// Draws the output primitives to the next frame of the given [`SwapChain`]. /// - /// [`SwapChain`]: #associatedtype.SwapChain - /// [`Surface`]: #associatedtype.Surface + /// [`SwapChain`]: Self::SwapChain 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, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], ) -> mouse::Interaction; } diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs new file mode 100644 index 00000000..34d70be3 --- /dev/null +++ b/graphics/src/window/gl_compositor.rs @@ -0,0 +1,63 @@ +use crate::{Color, Error, Size, Viewport}; +use iced_native::mouse; + +use core::ffi::c_void; + +/// A basic OpenGL compositor. +/// +/// A compositor is responsible for initializing a renderer and managing window +/// surfaces. +/// +/// For now, this compositor only deals with a single global surface +/// for drawing. However, the trait will most likely change in the near future +/// to handle multiple surfaces at once. +/// +/// If you implement an OpenGL renderer, you can implement this trait to ease +/// integration with existing windowing shells, like `iced_glutin`. +pub trait GLCompositor: Sized { + /// The renderer of the [`GLCompositor`]. + /// + /// This should point to your renderer type, which could be a type alias + /// of the [`Renderer`] provided in this crate with with a specific + /// [`Backend`]. + /// + /// [`Renderer`]: crate::Renderer + /// [`Backend`]: crate::Backend + type Renderer: iced_native::Renderer; + + /// The settings of the [`GLCompositor`]. + /// + /// It's up to you to decide the configuration supported by your renderer! + type Settings: Default; + + /// Creates a new [`GLCompositor`] and [`Renderer`] with the given + /// [`Settings`] and an OpenGL address loader function. + /// + /// [`Renderer`]: crate::Renderer + /// [`Backend`]: crate::Backend + /// [`Settings`]: Self::Settings + #[allow(unsafe_code)] + unsafe fn new( + settings: Self::Settings, + loader_function: impl FnMut(&str) -> *const c_void, + ) -> Result<(Self, Self::Renderer), Error>; + + /// Returns the amount of samples that should be used when configuring + /// an OpenGL context for this [`GLCompositor`]. + fn sample_count(settings: &Self::Settings) -> u32; + + /// Resizes the viewport of the [`GLCompositor`]. + fn resize_viewport(&mut self, physical_size: Size<u32>); + + /// Draws the provided output with the given [`Renderer`]. + /// + /// [`Renderer`]: crate::Renderer + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction; +} diff --git a/native/Cargo.toml b/native/Cargo.toml index 7e9c2a5a..2c99638a 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,22 +1,25 @@ [package] name = "iced_native" -version = "0.2.2" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A renderer-agnostic library for native GUIs" license = "MIT" repository = "https://github.com/hecrj/iced" +[features] +debug = [] + [dependencies] twox-hash = "1.5" -raw-window-handle = "0.3" unicode-segmentation = "1.6" +num-traits = "0.2" [dependencies.iced_core] -version = "0.2" +version = "0.3" path = "../core" [dependencies.iced_futures] -version = "0.1" +version = "0.2" path = "../futures" features = ["thread-pool"] diff --git a/native/README.md b/native/README.md index 31c7db88..6323dd4f 100644 --- a/native/README.md +++ b/native/README.md @@ -14,7 +14,9 @@ To achieve this, it introduces a bunch of reusable interfaces: - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic. - A `Windowed` 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. - +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> [documentation]: https://docs.rs/iced_native [`iced_core`]: ../core @@ -26,7 +28,7 @@ To achieve this, it introduces a bunch of reusable interfaces: Add `iced_native` as a dependency in your `Cargo.toml`: ```toml -iced_native = "0.2" +iced_native = "0.3" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index 4c574590..ecdccabf 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -2,7 +2,5 @@ /// 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/winit/src/debug/basic.rs b/native/src/debug/basic.rs index d46edba6..a42f66ea 100644 --- a/winit/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -1,5 +1,7 @@ +#![allow(missing_docs)] use std::{collections::VecDeque, time}; +/// A bunch of time measurements for debugging purposes. #[derive(Debug)] pub struct Debug { is_enabled: bool, @@ -30,6 +32,7 @@ pub struct Debug { } impl Debug { + /// Creates a new [`struct@Debug`]. pub fn new() -> Self { let now = time::Instant::now(); @@ -169,9 +172,13 @@ impl Debug { lines.push(key_value("Render:", self.render_durations.average())); lines.push(key_value("Message count:", self.message_count)); lines.push(String::from("Last messages:")); - lines.extend( - self.last_messages.iter().map(|msg| format!(" {}", msg)), - ); + lines.extend(self.last_messages.iter().map(|msg| { + if msg.len() <= 100 { + format!(" {}", msg) + } else { + format!(" {:.100}...", msg) + } + })); lines } diff --git a/winit/src/debug/null.rs b/native/src/debug/null.rs index 2a9430cd..60e6122d 100644 --- a/winit/src/debug/null.rs +++ b/native/src/debug/null.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] #[derive(Debug)] pub struct Debug; diff --git a/native/src/element.rs b/native/src/element.rs index 73e39012..d6e9639a 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,5 +1,8 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget, + Clipboard, Color, Hasher, Layout, Length, Point, Rectangle, Widget, }; /// A generic [`Widget`]. @@ -11,8 +14,6 @@ use crate::{ /// to turn it into an [`Element`]. /// /// [built-in widget]: widget/index.html#built-in-widgets -/// [`Widget`]: widget/trait.Widget.html -/// [`Element`]: struct.Element.html #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>, @@ -22,10 +23,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> where Renderer: crate::Renderer, { - /// Create a new [`Element`] containing the given [`Widget`]. - /// - /// [`Element`]: struct.Element.html - /// [`Widget`]: widget/trait.Widget.html + /// Creates a new [`Element`] containing the given [`Widget`]. pub fn new( widget: impl Widget<Message, Renderer> + 'a, ) -> Element<'a, Message, Renderer> { @@ -39,8 +37,6 @@ where /// This method is useful when you want to decouple different parts of your /// UI and make them __composable__. /// - /// [`Element`]: struct.Element.html - /// /// # Example /// Imagine we want to use [our counter](index.html#usage). But instead of /// showing a single counter, we want to display many of them. We can reuse @@ -186,8 +182,7 @@ where /// The [`Renderer`] will explain the layout of the [`Element`] graphically. /// This can be very useful for debugging your layout! /// - /// [`Element`]: struct.Element.html - /// [`Renderer`]: trait.Renderer.html + /// [`Renderer`]: crate::Renderer pub fn explain<C: Into<Color>>( self, color: C, @@ -202,23 +197,18 @@ where } /// Returns the width of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn width(&self) -> Length { self.widget.width() } /// Returns the height of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn height(&self) -> Length { self.widget.height() } /// Computes the layout of the [`Element`] in the given [`Limits`]. /// - /// [`Element`]: struct.Element.html - /// [`Limits`]: layout/struct.Limits.html + /// [`Limits`]: layout::Limits pub fn layout( &self, renderer: &Renderer, @@ -228,8 +218,6 @@ where } /// Processes a runtime [`Event`]. - /// - /// [`Event`]: enum.Event.html pub fn on_event( &mut self, event: Event, @@ -238,7 +226,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.widget.on_event( event, layout, @@ -246,30 +234,34 @@ where messages, renderer, clipboard, - ); + ) } /// Draws the [`Element`] and its children using the given [`Layout`]. - /// - /// [`Element`]: struct.Element.html - /// [`Layout`]: layout/struct.Layout.html pub fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } /// Computes the _layout_ hash of the [`Element`]. - /// - /// [`Element`]: struct.Element.html pub fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + /// Returns the overlay of the [`Element`], if there is any. + pub fn overlay<'b>( + &'b mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.widget.overlay(layout) + } } struct Map<'a, A, B, Renderer> { @@ -294,7 +286,9 @@ 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 - Renderer: crate::Renderer, + Renderer: crate::Renderer + 'a, + A: 'static, + B: 'static, { fn width(&self) -> Length { self.widget.width() @@ -320,10 +314,10 @@ where messages: &mut Vec<B>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let mut original_messages = Vec::new(); - self.widget.on_event( + let status = self.widget.on_event( event, layout, cursor_position, @@ -335,6 +329,8 @@ where original_messages .drain(..) .for_each(|message| messages.push((self.mapper)(message))); + + status } fn draw( @@ -343,14 +339,26 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { self.widget - .draw(renderer, defaults, layout, cursor_position) + .draw(renderer, defaults, layout, cursor_position, viewport) } fn hash_layout(&self, state: &mut Hasher) { self.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, B, Renderer>> { + let mapper = &self.mapper; + + self.widget + .overlay(layout) + .map(move |overlay| overlay.map(mapper)) + } } struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -396,7 +404,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.element.widget.on_event( event, layout, @@ -413,12 +421,14 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.explain( defaults, self.element.widget.as_ref(), layout, cursor_position, + viewport, self.color, ) } @@ -426,4 +436,11 @@ where fn hash_layout(&self, state: &mut Hasher) { self.element.widget.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.element.overlay(layout) + } } diff --git a/native/src/event.rs b/native/src/event.rs index 606a71d6..205bb797 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,4 +1,8 @@ -use crate::{keyboard, mouse, window}; +//! Handle events of a user interface. +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; /// A user interface event. /// @@ -6,7 +10,7 @@ use crate::{keyboard, mouse, window}; /// 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, Debug)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { /// A keyboard event Keyboard(keyboard::Event), @@ -16,4 +20,38 @@ pub enum Event { /// A window event Window(window::Event), + + /// A touch event + Touch(touch::Event), +} + +/// The status of an [`Event`] after being processed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Event`] was **NOT** handled by any widget. + Ignored, + + /// The [`Event`] was handled and processed by a widget. + Captured, +} + +impl Status { + /// Merges two [`Status`] into one. + /// + /// `Captured` takes precedence over `Ignored`: + /// + /// ``` + /// use iced_native::event::Status; + /// + /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); + /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); + /// ``` + pub fn merge(self, b: Self) -> Self { + match self { + Status::Ignored => b, + Status::Captured => Status::Captured, + } + } } diff --git a/native/src/layout.rs b/native/src/layout.rs index 4a3ab94a..6d144902 100644 --- a/native/src/layout.rs +++ b/native/src/layout.rs @@ -12,8 +12,6 @@ pub use node::Node; use crate::{Point, Rectangle, Vector}; /// The bounds of a [`Node`] and its children, using absolute coordinates. -/// -/// [`Node`]: struct.Node.html #[derive(Debug, Clone, Copy)] pub struct Layout<'a> { position: Point, @@ -34,14 +32,15 @@ impl<'a> Layout<'a> { } } - /// Gets the bounds of the [`Layout`]. + /// Returns the position of the [`Layout`]. + pub fn position(&self) -> Point { + self.position + } + + /// Returns the bounds of the [`Layout`]. /// /// The returned [`Rectangle`] describes the position and size of a /// [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Rectangle`]: struct.Rectangle.html - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { let bounds = self.node.bounds(); @@ -54,10 +53,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - /// - /// [`Layout`]: struct.Layout.html - /// [`Node`]: struct.Node.html - pub fn children(&'a self) -> impl Iterator<Item = Layout<'a>> { + pub fn children(self) -> impl Iterator<Item = Layout<'a>> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs index e4b21609..0759613f 100644 --- a/native/src/layout/debugger.rs +++ b/native/src/layout/debugger.rs @@ -1,8 +1,6 @@ -use crate::{Color, Layout, Point, Renderer, Widget}; +use crate::{Color, Layout, Point, Rectangle, Renderer, Widget}; /// A renderer able to graphically explain a [`Layout`]. -/// -/// [`Layout`]: struct.Layout.html pub trait Debugger: Renderer { /// Explains the [`Layout`] of an [`Element`] for debugging purposes. /// @@ -12,15 +10,15 @@ pub trait Debugger: Renderer { /// A common approach consists in recursively rendering the bounds of the /// [`Layout`] and its children. /// - /// [`Layout`]: struct.Layout.html - /// [`Element`]: ../struct.Element.html - /// [`Element::explain`]: ../struct.Element.html#method.explain + /// [`Element`]: crate::Element + /// [`Element::explain`]: crate::Element::explain fn explain<Message>( &mut self, defaults: &Self::Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, color: Color, ) -> Self::Output; } diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 9da75a21..4f6523fb 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -58,8 +58,6 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. -/// -/// [`Node`]: ../struct.Node.html pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index 664c881a..a7bb5c9c 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -17,9 +17,6 @@ impl Limits { }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub const fn new(min: Size, max: Size) -> Limits { Limits { min, @@ -29,32 +26,21 @@ impl Limits { } /// Returns the minimum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn min(&self) -> Size { self.min } /// Returns the maximum [`Size`] of the [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn max(&self) -> Size { 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 pub fn width(mut self, width: Length) -> Limits { match width { Length::Shrink => { @@ -77,8 +63,6 @@ impl Limits { } /// Applies a height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn height(mut self, height: Length) -> Limits { match height { Length::Shrink => { @@ -101,8 +85,6 @@ impl Limits { } /// Applies a minimum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_width(mut self, min_width: u32) -> Limits { self.min.width = self.min.width.max(min_width as f32).min(self.max.width); @@ -111,8 +93,6 @@ impl Limits { } /// Applies a maximum width constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_width(mut self, max_width: u32) -> Limits { self.max.width = self.max.width.min(max_width as f32).max(self.min.width); @@ -121,8 +101,6 @@ impl Limits { } /// Applies a minimum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn min_height(mut self, min_height: u32) -> Limits { self.min.height = self.min.height.max(min_height as f32).min(self.max.height); @@ -131,8 +109,6 @@ impl Limits { } /// Applies a maximum height constraint to the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn max_height(mut self, max_height: u32) -> Limits { self.max.height = self.max.height.min(max_height as f32).max(self.min.height); @@ -141,16 +117,11 @@ impl Limits { } /// Shrinks the current [`Limits`] to account for the given padding. - /// - /// [`Limits`]: struct.Limits.html pub fn pad(&self, padding: f32) -> Limits { self.shrink(Size::new(padding * 2.0, padding * 2.0)) } /// Shrinks the current [`Limits`] by the given [`Size`]. - /// - /// [`Limits`]: struct.Limits.html - /// [`Size`]: ../struct.Size.html pub fn shrink(&self, size: Size) -> Limits { let min = Size::new( (self.min().width - size.width).max(0.0), @@ -171,8 +142,6 @@ impl Limits { } /// Removes the minimum width constraint for the current [`Limits`]. - /// - /// [`Limits`]: struct.Limits.html pub fn loose(&self) -> Limits { Limits { min: Size::ZERO, @@ -183,8 +152,6 @@ impl Limits { /// Computes the resulting [`Size`] that fits the [`Limits`] given the /// intrinsic size of some content. - /// - /// [`Limits`]: struct.Limits.html pub fn resolve(&self, intrinsic_size: Size) -> Size { Size::new( intrinsic_size diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs index a265c46a..d7666f31 100644 --- a/native/src/layout/node.rs +++ b/native/src/layout/node.rs @@ -9,17 +9,11 @@ pub struct Node { impl Node { /// Creates a new [`Node`] with the given [`Size`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn new(size: Size) -> Self { Self::with_children(size, Vec::new()) } /// Creates a new [`Node`] with the given [`Size`] and children. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub const fn with_children(size: Size, children: Vec<Node>) -> Self { Node { bounds: Rectangle { @@ -33,30 +27,21 @@ impl Node { } /// Returns the [`Size`] of the [`Node`]. - /// - /// [`Node`]: struct.Node.html - /// [`Size`]: ../struct.Size.html pub fn size(&self) -> Size { Size::new(self.bounds.width, self.bounds.height) } /// Returns the bounds of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn bounds(&self) -> Rectangle { self.bounds } /// Returns the children of the [`Node`]. - /// - /// [`Node`]: struct.Node.html pub fn children(&self) -> &[Node] { &self.children } /// Aligns the [`Node`] in the given space. - /// - /// [`Node`]: struct.Node.html pub fn align( &mut self, horizontal_alignment: Align, @@ -85,8 +70,6 @@ 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 9882803f..0890785b 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,6 +1,6 @@ //! A renderer-agnostic native GUI runtime. //! -//!  +//!  //! //! `iced_native` takes [`iced_core`] and builds a native runtime on top of it, //! featuring: @@ -9,14 +9,11 @@ //! - Event handling for all the built-in widgets //! - A renderer-agnostic API //! -//! To achieve this, it introduces a bunch of reusable interfaces: +//! To achieve this, it introduces a couple of reusable interfaces: //! //! - 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 [`window::Backend`] 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. //! //! # Usage //! The strategy to use this crate depends on your particular use case. If you @@ -30,30 +27,39 @@ //! [`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle -//! [`Widget`]: widget/trait.Widget.html -//! [`window::Backend`]: window/trait.Backend.html -//! [`UserInterface`]: struct.UserInterface.html -//! [renderer]: renderer/index.html +//! [renderer]: crate::renderer #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] +pub mod event; pub mod keyboard; pub mod layout; pub mod mouse; +pub mod overlay; +pub mod program; pub mod renderer; pub mod subscription; +pub mod touch; pub mod widget; pub mod window; mod clipboard; mod element; -mod event; mod hasher; mod runtime; mod user_interface; +// We disable debug capabilities on release builds unless the `debug` feature +// is explicitly enabled. +#[cfg(feature = "debug")] +#[path = "debug/basic.rs"] +mod debug; +#[cfg(not(feature = "debug"))] +#[path = "debug/null.rs"] +mod debug; + pub use iced_core::{ Align, Background, Color, Font, HorizontalAlignment, Length, Point, Rectangle, Size, Vector, VerticalAlignment, @@ -64,10 +70,13 @@ pub use iced_futures::{executor, futures, Command}; pub use executor::Executor; pub use clipboard::Clipboard; +pub use debug::Debug; pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; +pub use overlay::Overlay; +pub use program::Program; pub use renderer::Renderer; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs index d27bc67e..6c8b61a5 100644 --- a/native/src/mouse/click.rs +++ b/native/src/mouse/click.rs @@ -36,8 +36,6 @@ impl Kind { impl Click { /// Creates a new [`Click`] with the given position and previous last /// [`Click`]. - /// - /// [`Click`]: struct.Click.html pub fn new(position: Point, previous: Option<Click>) -> Click { let time = Instant::now(); @@ -59,9 +57,6 @@ impl Click { } /// Returns the [`Kind`] of [`Click`]. - /// - /// [`Kind`]: enum.Kind.html - /// [`Click`]: struct.Click.html pub fn kind(&self) -> Kind { self.kind } diff --git a/native/src/overlay.rs b/native/src/overlay.rs new file mode 100644 index 00000000..ea8bb384 --- /dev/null +++ b/native/src/overlay.rs @@ -0,0 +1,76 @@ +//! Display interactive elements on top of other widgets. +mod element; + +pub mod menu; + +pub use element::Element; +pub use menu::Menu; + +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay<Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Returns the layout [`Node`] of the [`Overlay`]. + /// + /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + /// + /// [`Node`]: layout::Node + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node; + + /// Draws the [`Overlay`] using the associated `Renderer`. + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output; + + /// Computes the _layout_ hash of the [`Overlay`]. + /// + /// The produced hash is used by the runtime to decide if the [`Layout`] + /// needs to be recomputed between frames. Therefore, to ensure maximum + /// efficiency, the hash should only be affected by the properties of the + /// [`Overlay`] that can affect layouting. + /// + /// For example, the [`Text`] widget does not hash its color property, as + /// its value cannot affect the overall [`Layout`] of the user interface. + /// + /// [`Text`]: crate::widget::Text + fn hash_layout(&self, state: &mut Hasher, position: Point); + + /// Processes a runtime [`Event`]. + /// + /// It receives: + /// * an [`Event`] describing user interaction + /// * the computed [`Layout`] of the [`Overlay`] + /// * the current cursor position + /// * a mutable `Message` list, allowing the [`Overlay`] to produce + /// new messages based on user interaction. + /// * the `Renderer` + /// * a [`Clipboard`], if available + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + event::Status::Ignored + } +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs new file mode 100644 index 00000000..0f44a781 --- /dev/null +++ b/native/src/overlay/element.rs @@ -0,0 +1,156 @@ +pub use crate::Overlay; + +use crate::event::{self, Event}; +use crate::layout; +use crate::{Clipboard, Hasher, Layout, Point, Size, Vector}; + +/// A generic [`Overlay`]. +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Creates a new [`Element`] containing the given [`Overlay`]. + pub fn new( + position: Point, + overlay: Box<dyn Overlay<Message, Renderer> + 'a>, + ) -> Self { + Self { position, overlay } + } + + /// Translates the [`Element`]. + pub fn translate(mut self, translation: Vector) -> Self { + self.position = self.position + translation; + self + } + + /// Applies a transformation to the produced message of the [`Element`]. + pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: 'a, + B: 'static, + { + Element { + position: self.position, + overlay: Box::new(Map::new(self.overlay, f)), + } + } + + /// Computes the layout of the [`Element`] in the given bounds. + pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.overlay.layout(renderer, bounds, self.position) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + self.overlay.on_event( + event, + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + /// Draws the [`Element`] and its children using the given [`Layout`]. + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.overlay + .draw(renderer, defaults, layout, cursor_position) + } + + /// Computes the _layout_ hash of the [`Element`]. + pub fn hash_layout(&self, state: &mut Hasher) { + self.overlay.hash_layout(state, self.position); + } +} + +struct Map<'a, A, B, Renderer> { + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box<dyn Overlay<A, Renderer> + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.content.layout(renderer, bounds, position) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<B>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut original_messages = Vec::new(); + + let event_status = self.content.on_event( + event, + layout, + cursor_position, + &mut original_messages, + renderer, + clipboard, + ); + + original_messages + .drain(..) + .for_each(|message| messages.push((self.mapper)(message))); + + event_status + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + self.content + .draw(renderer, defaults, layout, cursor_position) + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + self.content.hash_layout(state, position); + } +} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs new file mode 100644 index 00000000..abac849f --- /dev/null +++ b/native/src/overlay/menu.rs @@ -0,0 +1,428 @@ +//! Build and show dropdown menus. +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::scrollable; +use crate::text; +use crate::{ + Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle, + Scrollable, Size, Vector, Widget, +}; + +/// A list of selectable options. +#[allow(missing_debug_implementations)] +pub struct Menu<'a, T, Renderer: self::Renderer> { + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + width: u16, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Renderer> Menu<'a, T, Renderer> +where + T: ToString + Clone, + Renderer: self::Renderer + 'a, +{ + /// Creates a new [`Menu`] with the given [`State`], a list of options, and + /// the message to produced when an option is selected. + pub fn new( + state: &'a mut State, + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + ) -> Self { + Menu { + state, + options, + hovered_option, + last_selection, + width: 0, + padding: 0, + text_size: None, + font: Default::default(), + style: Default::default(), + } + } + + /// Sets the width of the [`Menu`]. + pub fn width(mut self, width: u16) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`Menu`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`Menu`]. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the font of the [`Menu`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Menu`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Turns the [`Menu`] into an overlay [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + pub fn overlay<Message: 'a>( + self, + position: Point, + target_height: f32, + ) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new( + position, + Box::new(Overlay::new(self, target_height)), + ) + } +} + +/// The local state of a [`Menu`]. +#[derive(Debug, Clone, Default)] +pub struct State { + scrollable: scrollable::State, +} + +impl State { + /// Creates a new [`State`] for a [`Menu`]. + pub fn new() -> Self { + Self::default() + } +} + +struct Overlay<'a, Message, Renderer: self::Renderer> { + container: Container<'a, Message, Renderer>, + width: u16, + target_height: f32, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a, +{ + pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self + where + T: Clone + ToString, + { + let Menu { + state, + options, + hovered_option, + last_selection, + width, + padding, + font, + text_size, + style, + } = menu; + + let container = + Container::new(Scrollable::new(&mut state.scrollable).push(List { + options, + hovered_option, + last_selection, + font, + text_size, + padding, + style: style.clone(), + })) + .padding(1); + + Self { + container, + width: width, + target_height, + style: style, + } + } +} + +impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> + for Overlay<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let space_below = bounds.height - (position.y + self.target_height); + let space_above = position.y; + + let limits = layout::Limits::new( + Size::ZERO, + Size::new( + bounds.width - position.x, + if space_below > space_above { + space_below + } else { + space_above + }, + ), + ) + .width(Length::Units(self.width)); + + let mut node = self.container.layout(renderer, &limits); + + node.move_to(if space_below > space_above { + position + Vector::new(0.0, self.target_height) + } else { + position - Vector::new(0.0, node.size().height) + }); + + node + } + + fn hash_layout(&self, state: &mut Hasher, position: Point) { + use std::hash::Hash; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + (position.x as u32).hash(state); + (position.y as u32).hash(state); + self.container.hash_layout(state); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + self.container.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + let primitives = self.container.draw( + renderer, + defaults, + layout, + cursor_position, + &layout.bounds(), + ); + + renderer.decorate( + layout.bounds(), + cursor_position, + &self.style, + primitives, + ) + } +} + +struct List<'a, T, Renderer: self::Renderer> { + options: &'a [T], + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer> + for List<'a, T, Renderer> +where + T: Clone + ToString, + Renderer: self::Renderer, +{ + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits.width(Length::Fill).height(Length::Shrink); + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let size = { + let intrinsic = Size::new( + 0.0, + f32::from(text_size + self.padding * 2) + * self.options.len() as f32, + ); + + limits.resolve(intrinsic) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.options.len().hash(state); + self.text_size.hash(state); + self.padding.hash(state); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _messages: &mut Vec<Message>, + renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + if let Some(index) = *self.hovered_option { + if let Some(option) = self.options.get(index) { + *self.last_selection = Some(option.clone()); + } + } + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let bounds = layout.bounds(); + let text_size = + self.text_size.unwrap_or(renderer.default_size()); + + if bounds.contains(cursor_position) { + *self.hovered_option = Some( + ((cursor_position.y - bounds.y) + / f32::from(text_size + self.padding * 2)) + as usize, + ); + } + } + _ => {} + } + + event::Status::Ignored + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + viewport, + self.options, + *self.hovered_option, + self.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } +} + +/// The renderer of a [`Menu`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Menu`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: + scrollable::Renderer + container::Renderer + text::Renderer +{ + /// The [`Menu`] style supported by this renderer. + type Style: Default + Clone; + + /// Decorates a the list of options of a [`Menu`]. + /// + /// This method can be used to draw a background for the [`Menu`]. + fn decorate( + &mut self, + bounds: Rectangle, + cursor_position: Point, + style: &<Self as Renderer>::Style, + primitive: Self::Output, + ) -> Self::Output; + + /// Draws the list of options of a [`Menu`]. + fn draw<T: ToString>( + &mut self, + bounds: Rectangle, + cursor_position: Point, + viewport: &Rectangle, + options: &[T], + hovered_option: Option<usize>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> + for List<'a, T, Renderer> +where + T: ToString + Clone, + Message: 'a, + Renderer: 'a + self::Renderer, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/program.rs b/native/src/program.rs new file mode 100644 index 00000000..9ee72703 --- /dev/null +++ b/native/src/program.rs @@ -0,0 +1,30 @@ +//! Build interactive programs using The Elm Architecture. +use crate::{Command, Element, Renderer}; + +mod state; + +pub use state::State; + +/// The core of a user interface application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the [`Program`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/native/src/program/state.rs b/native/src/program/state.rs new file mode 100644 index 00000000..e630890a --- /dev/null +++ b/native/src/program/state.rs @@ -0,0 +1,180 @@ +use crate::{ + Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size, + UserInterface, +}; + +/// The execution state of a [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State<P> +where + P: Program + 'static, +{ + program: P, + cache: Option<Cache>, + primitive: <P::Renderer as Renderer>::Output, + queued_events: Vec<Event>, + queued_messages: Vec<P::Message>, +} + +impl<P> State<P> +where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + mut program: P, + bounds: Size, + cursor_position: Point, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let mut user_interface = build_user_interface( + &mut program, + Cache::default(), + renderer, + bounds, + debug, + ); + + debug.draw_started(); + let primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + let cache = Some(user_interface.into_cache()); + + State { + program, + cache, + primitive, + queued_events: Vec::new(), + queued_messages: Vec::new(), + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Returns a reference to the current rendering primitive of the [`State`]. + pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output { + &self.primitive + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns the [`Command`] obtained from [`Program`] after updating it, + /// only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor_position: Point, + clipboard: Option<&dyn Clipboard>, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Option<Command<P::Message>> { + let mut user_interface = build_user_interface( + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let _ = user_interface.update( + &self.queued_events, + cursor_position, + clipboard, + renderer, + &mut messages, + ); + + messages.extend(self.queued_messages.drain(..)); + self.queued_events.clear(); + debug.event_processing_finished(); + + if messages.is_empty() { + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + None + } else { + // When there are messages, we are forced to rebuild twice + // for now :^) + let temp_cache = user_interface.into_cache(); + + let commands = + Command::batch(messages.into_iter().map(|message| { + debug.log_message(&message); + + debug.update_started(); + let command = self.program.update(message); + debug.update_finished(); + + command + })); + + let mut user_interface = build_user_interface( + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + self.primitive = user_interface.draw(renderer, cursor_position); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + Some(commands) + } + } +} + +fn build_user_interface<'a, P: Program>( + program: &'a mut P, + cache: Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a16df72b..39a6cff1 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -13,19 +13,19 @@ //! In the end, a __renderer__ satisfying all the constraints is //! needed to build a [`UserInterface`]. //! -//! [`Widget`]: ../widget/trait.Widget.html -//! [`UserInterface`]: ../struct.UserInterface.html -//! [`Text`]: ../widget/text/struct.Text.html -//! [`text::Renderer`]: ../widget/text/trait.Renderer.html -//! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html -//! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html +//! [`Widget`]: crate::Widget +//! [`UserInterface`]: crate::UserInterface +//! [`Text`]: crate::widget::Text +//! [`text::Renderer`]: crate::widget::text::Renderer +//! [`Checkbox`]: crate::widget::Checkbox +//! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer #[cfg(debug_assertions)] mod null; #[cfg(debug_assertions)] pub use null::Null; -use crate::{layout, Element}; +use crate::{layout, Element, Rectangle}; /// A component that can take the state of a user interface and produce an /// output for its users. @@ -34,15 +34,11 @@ pub trait Renderer: Sized { /// /// If you are implementing a graphical renderer, your output will most /// likely be a tree of visual primitives. - /// - /// [`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. @@ -56,4 +52,13 @@ pub trait Renderer: Sized { ) -> layout::Node { element.layout(self, limits) } + + /// Overlays the `overlay` output with the given bounds on top of the `base` + /// output. + fn overlay( + &mut self, + base: Self::Output, + overlay: Self::Output, + overlay_bounds: Rectangle, + ) -> Self::Output; } diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 19689d30..91ee9a28 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,7 +1,8 @@ use crate::{ - button, checkbox, column, progress_bar, radio, row, scrollable, slider, - text, text_input, Color, Element, Font, HorizontalAlignment, Layout, Point, - Rectangle, Renderer, Size, VerticalAlignment, + button, checkbox, column, container, pane_grid, progress_bar, radio, row, + scrollable, slider, text, text_input, Color, Element, Font, + HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size, + VerticalAlignment, }; /// A renderer that does nothing. @@ -12,8 +13,6 @@ pub struct Null; impl Null { /// Creates a new [`Null`] renderer. - /// - /// [`Null`]: struct.Null.html pub fn new() -> Null { Null } @@ -22,6 +21,9 @@ impl Null { impl Renderer for Null { type Output = (); type Defaults = (); + + fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) { + } } impl column::Renderer for Null { @@ -31,6 +33,7 @@ impl column::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } @@ -42,6 +45,7 @@ impl row::Renderer for Null { _content: &[Element<'_, Message, Self>], _layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) { } } @@ -49,7 +53,9 @@ impl row::Renderer for Null { impl text::Renderer for Null { type Font = Font; - const DEFAULT_SIZE: u16 = 20; + fn default_size(&self) -> u16 { + 20 + } fn measure( &self, @@ -83,6 +89,9 @@ impl scrollable::Renderer for Null { _bounds: Rectangle, _content_bounds: Rectangle, _offset: u32, + _scrollbar_width: u16, + _scrollbar_margin: u16, + _scroller_width: u16, ) -> Option<scrollable::Scrollbar> { None } @@ -105,10 +114,6 @@ impl scrollable::Renderer for Null { 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 } @@ -195,9 +200,7 @@ impl checkbox::Renderer for Null { impl slider::Renderer for Null { type Style = (); - fn height(&self) -> u32 { - 30 - } + const DEFAULT_HEIGHT: u16 = 30; fn draw( &mut self, @@ -225,3 +228,60 @@ impl progress_bar::Renderer for Null { ) { } } + +impl container::Renderer for Null { + type Style = (); + + fn draw<Message>( + &mut self, + _defaults: &Self::Defaults, + _bounds: Rectangle, + _cursor_position: Point, + _viewport: &Rectangle, + _style: &Self::Style, + _content: &Element<'_, Message, Self>, + _content_layout: Layout<'_>, + ) { + } +} + +impl pane_grid::Renderer for Null { + fn draw<Message>( + &mut self, + _defaults: &Self::Defaults, + _content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)], + _dragging: Option<(pane_grid::Pane, Point)>, + _resizing: Option<pane_grid::Axis>, + _layout: Layout<'_>, + _cursor_position: Point, + ) { + } + + fn draw_pane<Message>( + &mut self, + _defaults: &Self::Defaults, + _bounds: Rectangle, + _style: &Self::Style, + _title_bar: Option<( + &pane_grid::TitleBar<'_, Message, Self>, + Layout<'_>, + )>, + _body: (&Element<'_, Message, Self>, Layout<'_>), + _cursor_position: Point, + ) { + } + + fn draw_title_bar<Message>( + &mut self, + _defaults: &Self::Defaults, + _bounds: Rectangle, + _style: &Self::Style, + _title: &str, + _title_size: u16, + _title_font: Self::Font, + _title_bounds: Rectangle, + _controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, + _cursor_position: Point, + ) { + } +} diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 9fa031f4..5b0a6925 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -1,12 +1,18 @@ //! Run commands and subscriptions. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::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>; +/// [`Command`]: crate::Command +/// [`Subscription`]: crate::Subscription +pub type Runtime<Executor, Receiver, Message> = iced_futures::Runtime< + Hasher, + (Event, event::Status), + Executor, + Receiver, + Message, +>; diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 0d002c6c..ff954382 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,5 +1,6 @@ //! Listen to external events in your application. -use crate::{Event, Hasher}; +use crate::event::{self, Event}; +use crate::Hasher; use iced_futures::futures::stream::BoxStream; /// A request to listen to external events. @@ -13,21 +14,18 @@ use iced_futures::futures::stream::BoxStream; /// 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>; +/// [`Command`]: crate::Command +pub type Subscription<T> = + iced_futures::Subscription<Hasher, (Event, event::Status), 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>; +pub type EventStream = BoxStream<'static, (Event, event::Status)>; /// A native [`Subscription`] tracker. -/// -/// [`Subscription`]: type.Subscription.html -pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>; +pub type Tracker = + iced_futures::subscription::Tracker<Hasher, (Event, event::Status)>; pub use iced_futures::subscription::Recipe; @@ -37,11 +35,30 @@ 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 +/// This subscription will notify your application of any [`Event`] that was +/// not captured by any widget. pub fn events() -> Subscription<Event> { - Subscription::from_recipe(Events) + Subscription::from_recipe(Events { + f: |event, status| match status { + event::Status::Ignored => Some(event), + event::Status::Captured => None, + }, + }) +} + +/// Returns a [`Subscription`] that filters all the runtime events with the +/// provided function, producing messages accordingly. +/// +/// This subscription will call the provided function for every [`Event`] +/// handled by the runtime. If the function: +/// +/// - Returns `None`, the [`Event`] will be discarded. +/// - Returns `Some` message, the `Message` will be produced. +pub fn events_with<Message>( + f: fn(Event, event::Status) -> Option<Message>, +) -> Subscription<Message> +where + Message: 'static + Send, +{ + Subscription::from_recipe(Events { f }) } diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs index ceae467d..f689f3af 100644 --- a/native/src/subscription/events.rs +++ b/native/src/subscription/events.rs @@ -1,18 +1,26 @@ -use crate::{ - subscription::{EventStream, Recipe}, - Event, Hasher, -}; +use crate::event::{self, Event}; +use crate::subscription::{EventStream, Recipe}; +use crate::Hasher; +use iced_futures::futures::future; +use iced_futures::futures::StreamExt; use iced_futures::BoxStream; -pub struct Events; +pub struct Events<Message> { + pub(super) f: fn(Event, event::Status) -> Option<Message>, +} -impl Recipe<Hasher, Event> for Events { - type Output = Event; +impl<Message> Recipe<Hasher, (Event, event::Status)> for Events<Message> +where + Message: 'static + Send, +{ + type Output = Message; fn hash(&self, state: &mut Hasher) { use std::hash::Hash; - std::any::TypeId::of::<Self>().hash(state); + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + self.f.hash(state); } fn stream( @@ -20,5 +28,9 @@ impl Recipe<Hasher, Event> for Events { event_stream: EventStream, ) -> BoxStream<Self::Output> { event_stream + .filter_map(move |(event, status)| { + future::ready((self.f)(event, status)) + }) + .boxed() } } diff --git a/native/src/touch.rs b/native/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/native/src/touch.rs @@ -0,0 +1,23 @@ +//! Build touch events. +use crate::Point; + +/// A touch interaction. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Event { + /// A touch interaction was started. + FingerPressed { id: Finger, position: Point }, + + /// An on-going touch interaction was moved. + FingerMoved { id: Finger, position: Point }, + + /// A touch interaction was ended. + FingerLifted { id: Finger, position: Point }, + + /// A touch interaction was canceled. + FingerLost { id: Finger, position: Point }, +} + +/// A unique identifier representing a finger on a touch interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Finger(pub u64); diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 48cd6111..7a64ac59 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,7 @@ -use crate::{layout, mouse, Clipboard, Element, Event, Layout, Point, Size}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Element, Layout, Point, Rectangle, Size}; use std::hash::Hasher; @@ -9,21 +12,17 @@ use std::hash::Hasher; /// Iced tries to avoid dictating how to write your event loop. You are in /// charge of using this type in your system in any way you want. /// -/// [`Layout`]: struct.Layout.html -/// /// # Example /// The [`integration` example] uses a [`UserInterface`] to integrate Iced in /// an existing graphical application. /// -/// [`integration` example]: https://github.com/hecrj/iced/tree/0.1/examples/integration -/// [`UserInterface`]: struct.UserInterface.html +/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { - hash: u64, root: Element<'a, Message, Renderer>, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, - cursor_position: Point, } impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> @@ -35,10 +34,6 @@ where /// It is able to avoid expensive computations when using a [`Cache`] /// obtained from a previous instance of a [`UserInterface`]. /// - /// [`Element`]: struct.Element.html - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html - /// /// # Example /// Imagine we want to build a [`UserInterface`] for /// [the counter example that we previously wrote](index.html#usage). Here @@ -95,25 +90,37 @@ where ) -> Self { let root = root.into(); - let hash = { - let hasher = &mut crate::Hasher::default(); - root.hash_layout(hasher); + let (base, overlay) = { + let hash = { + let hasher = &mut crate::Hasher::default(); + root.hash_layout(hasher); - hasher.finish() - }; + hasher.finish() + }; - let layout = if hash == cache.hash && bounds == cache.bounds { - cache.layout - } else { - renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) + let layout_is_cached = + hash == cache.base.hash && bounds == cache.bounds; + + let (layout, overlay) = if layout_is_cached { + (cache.base.layout, cache.overlay) + } else { + ( + renderer.layout( + &root, + &layout::Limits::new(Size::ZERO, bounds), + ), + None, + ) + }; + + (Layer { layout, hash }, overlay) }; UserInterface { - hash, root, - layout, + base, + overlay, bounds, - cursor_position: cache.cursor_position, } } @@ -122,15 +129,12 @@ where /// It returns __messages__ that may have been produced as a result of user /// interactions. You should feed these to your __update logic__. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Event`]: enum.Event.html - /// /// # Example /// Let's allow our [counter](index.html#usage) to change state by /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -152,12 +156,14 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// /// // Initialize our event storage /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -167,57 +173,103 @@ where /// ); /// /// // Update the user interface - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// let event_statuses = user_interface.update( + /// &events, + /// cursor_position, + /// None, + /// &renderer, + /// &mut messages + /// ); /// /// cache = user_interface.into_cache(); /// /// // Process the produced messages - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// } /// ``` pub fn update( &mut self, - events: impl IntoIterator<Item = Event>, + events: &[Event], + cursor_position: Point, clipboard: Option<&dyn Clipboard>, renderer: &Renderer, - ) -> Vec<Message> { - let mut messages = Vec::new(); - - for event in events { - if let Event::Mouse(mouse::Event::CursorMoved { x, y }) = event { - self.cursor_position = Point::new(x, y); - } - - self.root.widget.on_event( - event, - Layout::new(&self.layout), - self.cursor_position, - &mut messages, + messages: &mut Vec<Message>, + ) -> Vec<event::Status> { + let (base_cursor, overlay_statuses) = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, renderer, - clipboard, ); - } - messages + let event_statuses = events + .iter() + .cloned() + .map(|event| { + overlay.on_event( + event, + Layout::new(&layer.layout), + cursor_position, + messages, + renderer, + clipboard, + ) + }) + .collect(); + + let base_cursor = if layer.layout.bounds().contains(cursor_position) + { + // TODO: Type-safe cursor availability + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + self.overlay = Some(layer); + + (base_cursor, event_statuses) + } else { + (cursor_position, vec![event::Status::Ignored; events.len()]) + }; + + events + .iter() + .cloned() + .zip(overlay_statuses.into_iter()) + .map(|(event, overlay_status)| { + let event_status = self.root.widget.on_event( + event, + Layout::new(&self.base.layout), + base_cursor, + messages, + renderer, + clipboard, + ); + + event_status.merge(overlay_status) + }) + .collect() } /// Draws the [`UserInterface`] with the provided [`Renderer`]. /// - /// It returns the current state of the [`MouseCursor`]. You should update - /// the icon of the mouse cursor accordingly in your system. + /// It returns the some [`Renderer::Output`]. You should update the icon of + /// the mouse cursor accordingly in your system. /// - /// [`UserInterface`]: struct.UserInterface.html - /// [`Renderer`]: trait.Renderer.html - /// [`MouseCursor`]: enum.MouseCursor.html + /// [`Renderer`]: crate::Renderer + /// [`Renderer::Output`]: crate::Renderer::Output /// /// # Example /// We can finally draw our [counter](index.html#usage) by /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{UserInterface, Cache, Size}; + /// use iced_native::{UserInterface, Cache, Size, Point}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -239,10 +291,12 @@ where /// let mut cache = Cache::new(); /// let mut renderer = Renderer::new(); /// let mut window_size = Size::new(1024.0, 768.0); + /// let mut cursor_position = Point::default(); /// let mut events = Vec::new(); + /// let mut messages = Vec::new(); /// /// loop { - /// // Process system events... + /// // Obtain system events... /// /// let mut user_interface = UserInterface::build( /// counter.view(), @@ -251,14 +305,21 @@ where /// &mut renderer, /// ); /// - /// let messages = user_interface.update(events.drain(..), None, &renderer); + /// // Update the user interface + /// let event_statuses = user_interface.update( + /// &events, + /// cursor_position, + /// None, + /// &renderer, + /// &mut messages + /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer); + /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position); /// /// cache = user_interface.into_cache(); /// - /// for message in messages { + /// for message in messages.drain(..) { /// counter.update(message); /// } /// @@ -266,39 +327,132 @@ where /// // Flush rendering operations... /// } /// ``` - pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output { - self.root.widget.draw( + pub fn draw( + &mut self, + renderer: &mut Renderer, + cursor_position: Point, + ) -> Renderer::Output { + let viewport = Rectangle::with_size(self.bounds); + + let overlay = if let Some(mut overlay) = + self.root.overlay(Layout::new(&self.base.layout)) + { + let layer = Self::overlay_layer( + self.overlay.take(), + self.bounds, + &mut overlay, + renderer, + ); + + let overlay_bounds = layer.layout.bounds(); + + let overlay_primitives = overlay.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&layer.layout), + cursor_position, + ); + + self.overlay = Some(layer); + + Some((overlay_primitives, overlay_bounds)) + } else { + None + }; + + if let Some((overlay_primitives, overlay_bounds)) = overlay { + let base_cursor = if overlay_bounds.contains(cursor_position) { + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + let base_primitives = self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + base_cursor, + &viewport, + ); + + renderer.overlay( + base_primitives, + overlay_primitives, + overlay_bounds, + ) + } else { + self.root.widget.draw( + renderer, + &Renderer::Defaults::default(), + Layout::new(&self.base.layout), + cursor_position, + &viewport, + ) + } + } + + /// Relayouts and returns a new [`UserInterface`] using the provided + /// bounds. + pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { + Self::build( + self.root, + bounds, + Cache { + base: self.base, + overlay: self.overlay, + bounds: self.bounds, + }, renderer, - &Renderer::Defaults::default(), - Layout::new(&self.layout), - self.cursor_position, ) } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn into_cache(self) -> Cache { Cache { - hash: self.hash, - layout: self.layout, + base: self.base, + overlay: self.overlay, bounds: self.bounds, - cursor_position: self.cursor_position, + } + } + + fn overlay_layer( + cache: Option<Layer>, + bounds: Size, + overlay: &mut overlay::Element<'_, Message, Renderer>, + renderer: &Renderer, + ) -> Layer { + let new_hash = { + let hasher = &mut crate::Hasher::default(); + overlay.hash_layout(hasher); + + hasher.finish() + }; + + let layout = match cache { + Some(Layer { hash, layout }) if new_hash == hash => layout, + _ => overlay.layout(renderer, bounds), + }; + + Layer { + layout, + hash: new_hash, } } } +#[derive(Debug, Clone)] +struct Layer { + layout: layout::Node, + hash: u64, +} + /// Reusable data of a specific [`UserInterface`]. -/// -/// [`UserInterface`]: struct.UserInterface.html #[derive(Debug, Clone)] pub struct Cache { - hash: u64, - layout: layout::Node, + base: Layer, + overlay: Option<Layer>, bounds: Size, - cursor_position: Point, } impl Cache { @@ -306,15 +460,14 @@ impl Cache { /// /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. - /// - /// [`Cache`]: struct.Cache.html - /// [`UserInterface`]: struct.UserInterface.html pub fn new() -> Cache { Cache { - hash: 0, - layout: layout::Node::new(Size::new(0.0, 0.0)), + base: Layer { + layout: layout::Node::new(Size::new(0.0, 0.0)), + hash: 0, + }, + overlay: None, bounds: Size::ZERO, - cursor_position: Point::new(-1.0, -1.0), } } } @@ -324,11 +477,3 @@ impl Default for Cache { Cache::new() } } - -impl PartialEq for Cache { - fn eq(&self, other: &Cache) -> bool { - self.hash == other.hash && self.cursor_position == other.cursor_position - } -} - -impl Eq for Cache {} diff --git a/native/src/widget.rs b/native/src/widget.rs index 4453145b..3677713a 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -18,17 +18,18 @@ //! use iced_native::{button, Button, Widget}; //! ``` //! -//! [`Widget`]: trait.Widget.html -//! [renderer]: ../renderer/index.html +//! [renderer]: crate::renderer pub mod button; pub mod checkbox; pub mod column; pub mod container; pub mod image; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; pub mod row; +pub mod rule; pub mod scrollable; pub mod slider; pub mod space; @@ -49,12 +50,16 @@ pub use image::Image; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; #[doc(no_inline)] pub use row::Row; #[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] pub use scrollable::Scrollable; #[doc(no_inline)] pub use slider::Slider; @@ -67,16 +72,16 @@ pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; -use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// A component that displays information and allows interaction. /// /// If you want to build your own widgets, you will need to implement this /// trait. /// -/// [`Widget`]: trait.Widget.html -/// [`Element`]: ../struct.Element.html -/// /// # Examples /// The repository has some [examples] showcasing how to implement a custom /// widget: @@ -88,24 +93,20 @@ use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.1/examples/bezier_tool -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.1/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.1/examples/geometry +/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.1/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, { /// Returns the width of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn width(&self) -> Length; /// Returns the height of the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn height(&self) -> Length; /// Returns the [`Node`] of the [`Widget`]. @@ -113,9 +114,7 @@ where /// This [`Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. /// - /// [`Node`]: ../layout/struct.Node.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html + /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -123,14 +122,13 @@ where ) -> layout::Node; /// Draws the [`Widget`] using the associated `Renderer`. - /// - /// [`Widget`]: trait.Widget.html fn draw( &self, renderer: &mut Renderer, defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output; /// Computes the _layout_ hash of the [`Widget`]. @@ -143,9 +141,7 @@ where /// For example, the [`Text`] widget does not hash its color property, as /// its value cannot affect the overall [`Layout`] of the user interface. /// - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Text`]: text/struct.Text.html + /// [`Text`]: crate::widget::Text fn hash_layout(&self, state: &mut Hasher); /// Processes a runtime [`Event`]. @@ -160,11 +156,6 @@ where /// * a [`Clipboard`], if available /// /// By default, it does nothing. - /// - /// [`Event`]: ../enum.Event.html - /// [`Widget`]: trait.Widget.html - /// [`Layout`]: ../layout/struct.Layout.html - /// [`Clipboard`]: ../trait.Clipboard.html fn on_event( &mut self, _event: Event, @@ -173,6 +164,15 @@ where _messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the overlay of the [`Widget`], if there is any. + fn overlay( + &mut self, + _layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + None } } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index c932da2b..8e2450de 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -1,12 +1,12 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -18,6 +18,7 @@ use std::hash::Hash; /// # type Button<'a, Message> = /// # iced_native::Button<'a, Message, iced_native::renderer::Null>; /// # +/// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } @@ -41,13 +42,11 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { impl<'a, Message, Renderer> Button<'a, Message, Renderer> where + Message: Clone, Renderer: self::Renderer, { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -66,56 +65,42 @@ where } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; 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 pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn padding(mut self, padding: u16) -> Self { self.padding = padding; self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html pub fn on_press(mut self, msg: Message) -> Self { 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 @@ -123,8 +108,6 @@ where } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_pressed: bool, @@ -132,8 +115,6 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -142,8 +123,8 @@ impl State { impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -182,31 +163,43 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if self.on_press.is_some() { let bounds = layout.bounds(); - self.state.is_pressed = bounds.contains(cursor_position); + if bounds.contains(cursor_position) { + self.state.is_pressed = true; + + return event::Status::Captured; + } } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + if self.state.is_pressed { + self.state.is_pressed = false; - self.state.is_pressed = false; + if bounds.contains(cursor_position) { + messages.push(on_press); + } - if is_clicked { - messages.push(on_press); + return event::Status::Captured; } } } + Event::Touch(touch::Event::FingerLost { .. }) => { + self.state.is_pressed = false; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -215,6 +208,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, @@ -242,20 +236,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Button`] in your user interface. /// -/// [`Button`]: struct.Button.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer 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<Message>( &mut self, defaults: &Self::Defaults, @@ -272,8 +261,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( button: Button<'a, Message, Renderer>, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 5fb13290..77a82fad 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -1,10 +1,15 @@ //! Show toggle controls using checkboxes. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -32,7 +37,8 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, + font: Renderer::Font, style: Renderer::Style, } @@ -47,8 +53,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, @@ -60,46 +64,45 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, + font: Renderer::Font::default(), style: Renderer::Style::default(), } } /// 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 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.text_size = Some(text_size); self } - /// Sets the style of the [`Checkbox`]. + /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Checkbox`]: struct.Checkbox.html + /// [`Font`]: crate::widget::text::Renderer::Font + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Checkbox`]. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -135,8 +138,9 @@ where ) .push( Text::new(&self.label) + .font(self.font) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -149,17 +153,22 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { messages.push((self.on_toggle)(!self.is_checked)); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -168,6 +177,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -181,8 +191,8 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, - Default::default(), + self.text_size.unwrap_or(renderer.default_size()), + self.font, None, HorizontalAlignment::Left, VerticalAlignment::Center, @@ -213,20 +223,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Checkbox`] in your user interface. /// -/// [`Checkbox`]: struct.Checkbox.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Checkbox`]. - /// - /// [`Checkbox`]: struct.Checkbox.html const DEFAULT_SPACING: u16; /// Draws a [`Checkbox`]. @@ -236,8 +241,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Checkbox`] is selected or not /// * whether the mouse is over the [`Checkbox`] or not /// * the drawn label of the [`Checkbox`] - /// - /// [`Checkbox`]: struct.Checkbox.html fn draw( &mut self, bounds: Rectangle, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 259a7e6e..e0e88d31 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,18 +1,16 @@ //! Distribute content vertically. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; /// A container that distributes its contents vertically. -/// -/// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, @@ -27,15 +25,11 @@ pub struct Column<'a, Message, Renderer> { impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,56 +56,42 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -164,9 +144,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -175,8 +157,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -185,8 +167,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -199,11 +188,23 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Column`]. @@ -211,8 +212,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Column`] in your user interface. /// -/// [`Column`]: struct.Column.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Column`]. /// @@ -220,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Column`] /// - the [`Layout`] of the [`Column`] and its children /// - the cursor position - /// - /// [`Column`]: struct.Column.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 2590fe3b..65764148 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -1,9 +1,11 @@ //! Decorate content and apply alignment. use std::hash::Hash; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::u32; @@ -29,8 +31,6 @@ where Renderer: self::Renderer, { /// Creates an empty [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn new<T>(content: T) -> Self where T: Into<Element<'a, Message, Renderer>>, @@ -49,80 +49,60 @@ where } /// Sets the padding of the [`Container`]. - /// - /// [`Container`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the content alignment for the horizontal axis of the [`Container`]. - /// - /// [`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 } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`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 @@ -174,7 +154,7 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { self.content.widget.on_event( event, layout.children().next().unwrap(), @@ -191,11 +171,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( defaults, layout.bounds(), cursor_position, + viewport, &self.style, &self.content, layout.children().next().unwrap(), @@ -214,6 +196,13 @@ where self.content.hash_layout(state); } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.content.overlay(layout.children().next().unwrap()) + } } /// The renderer of a [`Container`]. @@ -221,20 +210,18 @@ where /// 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 +/// [renderer]: crate::renderer 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, + viewport: &Rectangle, style: &Self::Style, content: &Element<'_, Message, Self>, content_layout: Layout<'_>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 49905830..4d8e0a3f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -2,7 +2,8 @@ pub mod viewer; pub use viewer::Viewer; -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -30,8 +31,6 @@ pub struct Image { impl Image { /// Creates a new [`Image`] with the given path. - /// - /// [`Image`]: struct.Image.html pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), @@ -41,16 +40,12 @@ impl Image { } /// Sets the width of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -100,6 +95,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -115,8 +111,6 @@ where } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -127,8 +121,6 @@ impl Handle { /// Creates an image [`Handle`] pointing to the image of the given path. /// /// Makes an educated guess about the image format by examining the data in the file. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -138,8 +130,6 @@ impl Handle { /// 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, @@ -154,8 +144,6 @@ impl Handle { /// /// 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)) } @@ -171,15 +159,11 @@ impl Handle { } /// 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 } @@ -201,8 +185,6 @@ impl Hash for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -239,17 +221,12 @@ impl std::fmt::Debug for Data { /// Your [renderer] will need to implement this trait before being able to use /// an [`Image`] in your user interface. /// -/// [`Image`]: struct.Image.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the dimensions of an [`Image`] located on the given path. - /// - /// [`Image`]: struct.Image.html fn dimensions(&self, handle: &Handle) -> (u32, u32); /// Draws an [`Image`]. - /// - /// [`Image`]: struct.Image.html fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output; } diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index b129924b..9544beab 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -1,7 +1,11 @@ //! Zoom and pan on an image. +use crate::event::{self, Event}; +use crate::image; +use crate::layout; +use crate::mouse; use crate::{ - image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Size, Vector, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; use std::{f32, hash::Hash, u32}; @@ -216,7 +220,7 @@ where _messages: &mut Vec<Message>, renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); @@ -290,11 +294,16 @@ where self.state.starting_cursor_pos = None } } - Event::Mouse(mouse::Event::CursorMoved { x, y }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) => { if self.state.is_cursor_clicked() { let image_bounds = self.image_bounds(renderer, bounds); - self.state.pan(x, y, bounds, image_bounds); + self.state.pan( + position.x, + position.y, + bounds, + image_bounds, + ); } } _ => {} @@ -305,6 +314,8 @@ where self.state.starting_cursor_pos = None; } } + + event::Status::Ignored } fn draw( @@ -313,6 +324,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 076ae76f..ff19cbc2 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,33 +6,43 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -//! [`PaneGrid`]: struct.PaneGrid.html +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid mod axis; +mod configuration; mod content; mod direction; mod node; mod pane; mod split; mod state; +mod title_bar; pub use axis::Axis; +pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; pub use node::Node; pub use pane::Pane; pub use split::Split; -pub use state::{Focus, State}; - +pub use state::State; +pub use title_bar::TitleBar; + +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::row; +use crate::text; use crate::{ - keyboard, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, - Point, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector, + Widget, }; /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// -/// [](https://gfycat.com/mixedflatjellyfish) +/// [](https://gfycat.com/frailfreshairedaleterrier) /// /// This distribution of space is common in tiling window managers (like /// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even @@ -69,134 +79,89 @@ use crate::{ /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state, focus| { -/// match state { +/// PaneGrid::new(&mut state, |pane, state| { +/// pane_grid::Content::new(match state { /// PaneState::SomePane => Text::new("This is some pane"), /// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), -/// }.into() +/// }) /// }) /// .on_drag(Message::PaneDragged) -/// .on_resize(Message::PaneResized); +/// .on_resize(10, Message::PaneResized); /// ``` -/// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`State`]: struct.State.html #[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> { +pub struct PaneGrid<'a, Message, Renderer: self::Renderer> { state: &'a mut state::Internal, - pressed_modifiers: &'a mut keyboard::ModifiersState, - elements: Vec<(Pane, Element<'a, Message, Renderer>)>, + elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, spacing: u16, - modifier_keys: keyboard::ModifiersState, + on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, - on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message + 'a>>, - on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message> + 'a>>, + on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// /// The view function will be called to display each [`Pane`] present in the /// [`State`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new<T>( state: &'a mut State<T>, - view: impl Fn( - Pane, - &'a mut T, - Option<Focus>, - ) -> Element<'a, Message, Renderer>, + view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { - let action = state.internal.action(); - let current_focus = action.focus(); - state .panes .iter_mut() - .map(move |(pane, pane_state)| { - let focus = match current_focus { - Some((focused_pane, focus)) - if *pane == focused_pane => - { - Some(focus) - } - _ => None, - }; - - (*pane, view(*pane, pane_state, focus)) - }) + .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; Self { state: &mut state.internal, - pressed_modifiers: &mut state.modifiers, elements, width: Length::Fill, height: Length::Fill, spacing: 0, - modifier_keys: keyboard::ModifiersState { - control: true, - ..Default::default() - }, + on_click: None, on_drag: None, on_resize: None, - on_key_press: None, } } /// Sets the width of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn spacing(mut self, units: u16) -> Self { self.spacing = units; self } - /// Sets the modifier keys of the [`PaneGrid`]. - /// - /// The modifier keys will need to be pressed to trigger dragging, resizing, - /// and key events. - /// - /// The default modifier key is `Ctrl`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn modifier_keys( - mut self, - modifier_keys: keyboard::ModifiersState, - ) -> Self { - self.modifier_keys = modifier_keys; + /// Sets the message that will be produced when a [`Pane`] of the + /// [`PaneGrid`] is clicked. + pub fn on_click<F>(mut self, f: F) -> Self + where + F: 'a + Fn(Pane) -> Message, + { + self.on_click = Some(Box::new(f)); self } /// Enables the drag and drop interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. - /// - /// Panes can be dragged using `Modifier keys + Left click`. - /// - /// [`PaneGrid`]: struct.PaneGrid.html pub fn on_drag<F>(mut self, f: F) -> Self where F: 'a + Fn(DragEvent) -> Message, @@ -208,40 +173,54 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { /// Enables the resize interactions of the [`PaneGrid`], which will /// use the provided function to produce messages. /// - /// Panes can be resized using `Modifier keys + Right click`. + /// The `leeway` describes the amount of space around a split that can be + /// used to grab it. /// - /// [`PaneGrid`]: struct.PaneGrid.html - pub fn on_resize<F>(mut self, f: F) -> Self + /// The grabbable area of a split will have a length of `spacing + leeway`, + /// properly centered. In other words, a length of + /// `(spacing + leeway) / 2.0` on either side of the split line. + pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self where F: 'a + Fn(ResizeEvent) -> Message, { - self.on_resize = Some(Box::new(f)); + self.on_resize = Some((leeway, Box::new(f))); self } +} - /// Captures hotkey interactions with the [`PaneGrid`], using the provided - /// function to produce messages. - /// - /// The function will be called when: - /// - a [`Pane`] is focused - /// - a key is pressed - /// - all the modifier keys are pressed - /// - /// If the function returns `None`, the key press event will be discarded - /// without producing any message. - /// - /// This method is particularly useful to implement hotkey interactions. - /// For instance, you can use it to enable splitting, swapping, or resizing - /// panes by pressing combinations of keys. - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - pub fn on_key_press<F>(mut self, f: F) -> Self - where - F: 'a + Fn(KeyPressEvent) -> Option<Message>, - { - self.on_key_press = Some(Box::new(f)); - self +impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + fn click_pane( + &mut self, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + ) { + let mut clicked_region = + self.elements.iter().zip(layout.children()).filter( + |(_, layout)| layout.bounds().contains(cursor_position), + ); + + if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(on_click) = &self.on_click { + messages.push(on_click(*pane)); + } + + if let Some(on_drag) = &self.on_drag { + if content.can_be_picked_at(layout, cursor_position) { + let pane_position = layout.position(); + + let origin = cursor_position + - Vector::new(pane_position.x, pane_position.y); + + self.state.pick_pane(pane, origin); + + messages.push(on_drag(DragEvent::Picked { pane: *pane })); + } + } + } } fn trigger_resize( @@ -249,12 +228,12 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { layout: Layout<'_>, cursor_position: Point, messages: &mut Vec<Message>, - ) { - if let Some(on_resize) = &self.on_resize { + ) -> event::Status { + if let Some((_, on_resize)) = &self.on_resize { if let Some((split, _)) = self.state.picked_split() { let bounds = layout.bounds(); - let splits = self.state.splits( + let splits = self.state.split_regions( f32::from(self.spacing), Size::new(bounds.width, bounds.height), ); @@ -276,89 +255,59 @@ impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> { }; messages.push(on_resize(ResizeEvent { split, ratio })); + + return event::Status::Captured; } } } + + event::Status::Ignored } } /// An event produced during a drag and drop interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub enum DragEvent { /// A [`Pane`] was picked for dragging. - /// - /// [`Pane`]: struct.Pane.html Picked { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, /// A [`Pane`] was dropped on top of another [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Dropped { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, /// The [`Pane`] where the picked one was dropped on. - /// - /// [`Pane`]: struct.Pane.html target: Pane, }, /// A [`Pane`] was picked and then dropped outside of other [`Pane`] /// boundaries. - /// - /// [`Pane`]: struct.Pane.html Canceled { /// The picked [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html pane: Pane, }, } /// An event produced during a resize interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html #[derive(Debug, Clone, Copy)] pub struct ResizeEvent { /// The [`Split`] that is being dragged for resizing. - /// - /// [`Split`]: struct.Split.html pub split: Split, /// The new ratio of the [`Split`]. /// /// The ratio is a value in [0, 1], representing the exact position of a /// [`Split`] between two panes. - /// - /// [`Split`]: struct.Split.html pub ratio: f32, } -/// An event produced during a key press interaction of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone, Copy)] -pub struct KeyPressEvent { - /// The key that was pressed. - pub key_code: keyboard::KeyCode, - - /// The state of the modifier keys when the key was pressed. - pub modifiers: keyboard::ModifiersState, -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for PaneGrid<'a, Message, Renderer> where - Renderer: self::Renderer, + Renderer: self::Renderer + container::Renderer, { fn width(&self) -> Length { self.width @@ -376,7 +325,7 @@ where let limits = limits.width(self.width).height(self.height); let size = limits.resolve(Size::ZERO); - let regions = self.state.regions(f32::from(self.spacing), size); + let regions = self.state.pane_regions(f32::from(self.spacing), size); let children = self .elements @@ -405,42 +354,57 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { + let mut event_status = event::Status::Ignored; + match event { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { - let mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - if let Some(((pane, _), _)) = clicked_region.next() { - match &self.on_drag { - Some(on_drag) - if self - .pressed_modifiers - .matches(self.modifier_keys) => - { - self.state.pick_pane(pane); - - messages.push(on_drag(DragEvent::Picked { - pane: *pane, - })); + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self.state.split_regions( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis)) = clicked_split { + self.state.pick_split(&split, axis); + } else { + self.click_pane( + layout, + cursor_position, + messages, + ); + } } - _ => { - self.state.focus(pane); + None => { + self.click_pane( + layout, + cursor_position, + messages, + ); } } - } else { - self.state.unfocus(); } } mouse::Event::ButtonReleased(mouse::Button::Left) => { - if let Some(pane) = self.state.picked_pane() { - self.state.focus(&pane); - + if let Some((pane, _)) = self.state.picked_pane() { if let Some(on_drag) = &self.on_drag { let mut dropped_region = self .elements @@ -462,131 +426,42 @@ where messages.push(on_drag(event)); } - } - } - mouse::Event::ButtonPressed(mouse::Button::Right) - if self.on_resize.is_some() - && self.state.picked_pane().is_none() - && self - .pressed_modifiers - .matches(self.modifier_keys) => - { - let bounds = layout.bounds(); - if bounds.contains(cursor_position) { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.splits( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let mut sorted_splits: Vec<_> = splits - .iter() - .filter(|(_, (axis, rectangle, _))| match axis { - Axis::Horizontal => { - relative_cursor.x > rectangle.x - && relative_cursor.x - < rectangle.x + rectangle.width - } - Axis::Vertical => { - relative_cursor.y > rectangle.y - && relative_cursor.y - < rectangle.y + rectangle.height - } - }) - .collect(); - - sorted_splits.sort_by_key( - |(_, (axis, rectangle, ratio))| { - let distance = match axis { - Axis::Horizontal => (relative_cursor.y - - (rectangle.y - + rectangle.height * ratio)) - .abs(), - Axis::Vertical => (relative_cursor.x - - (rectangle.x - + rectangle.width * ratio)) - .abs(), - }; - - distance.round() as u32 - }, - ); - - if let Some((split, (axis, _, _))) = - sorted_splits.first() - { - self.state.pick_split(split, *axis); - self.trigger_resize( - layout, - cursor_position, - messages, - ); - } + self.state.idle(); + + event_status = event::Status::Captured; + } else if self.state.picked_split().is_some() { + self.state.idle(); + + event_status = event::Status::Captured; } } - mouse::Event::ButtonReleased(mouse::Button::Right) - if self.state.picked_split().is_some() => - { - self.state.drop_split(); - } mouse::Event::CursorMoved { .. } => { - self.trigger_resize(layout, cursor_position, messages); + event_status = + self.trigger_resize(layout, cursor_position, messages); } _ => {} }, - Event::Keyboard(keyboard_event) => { - match keyboard_event { - keyboard::Event::KeyPressed { - modifiers, - key_code, - } => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); - } - } - } - } - - *self.pressed_modifiers = modifiers; - } - keyboard::Event::KeyReleased { modifiers, .. } => { - *self.pressed_modifiers = modifiers; - } - _ => {} - } - } _ => {} } if self.state.picked_pane().is_none() { - { - self.elements.iter_mut().zip(layout.children()).for_each( - |((_, pane), layout)| { - pane.widget.on_event( - event.clone(), - layout, - cursor_position, - messages, - renderer, - clipboard, - ) - }, - ); - } + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((_, pane), layout)| { + pane.on_event( + event.clone(), + layout, + cursor_position, + messages, + renderer, + clipboard, + ) + }) + .fold(event_status, event::Status::merge) + } else { + event::Status::Captured } } @@ -596,12 +471,40 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw( + let picked_split = self + .state + .picked_split() + .or_else(|| match self.on_resize { + Some((leeway, _)) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = self + .state + .split_regions(f32::from(self.spacing), bounds.size()); + + hovered_split( + splits.iter(), + f32::from(self.spacing + leeway), + relative_cursor, + ) + } + None => None, + }) + .map(|(_, axis)| axis); + + self::Renderer::draw( + renderer, defaults, &self.elements, self.state.picked_pane(), - self.state.picked_split().map(|(_, axis)| axis), + picked_split, layout, cursor_position, ) @@ -609,6 +512,7 @@ where fn hash_layout(&self, state: &mut Hasher) { use std::hash::Hash; + struct Marker; std::any::TypeId::of::<Marker>().hash(state); @@ -620,6 +524,17 @@ where element.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter_mut() + .zip(layout.children()) + .filter_map(|((_, pane), layout)| pane.overlay(layout)) + .next() + } } /// The renderer of a [`PaneGrid`]. @@ -627,9 +542,10 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`PaneGrid`] in your user interface. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: + crate::Renderer + container::Renderer + text::Renderer + Sized +{ /// Draws a [`PaneGrid`]. /// /// It receives: @@ -638,25 +554,59 @@ pub trait Renderer: crate::Renderer + Sized { /// - the [`Axis`] that is currently being resized /// - the [`Layout`] of the [`PaneGrid`] and its elements /// - the cursor position - /// - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`Pane`]: struct.Pane.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, - content: &[(Pane, Element<'_, Message, Self>)], - dragging: Option<Pane>, + content: &[(Pane, Content<'_, Message, Self>)], + dragging: Option<(Pane, Point)>, resizing: Option<Axis>, layout: Layout<'_>, cursor_position: Point, ) -> Self::Output; + + /// Draws a [`Pane`]. + /// + /// It receives: + /// - the [`TitleBar`] of the [`Pane`], if any + /// - the [`Content`] of the [`Pane`] + /// - the [`Layout`] of the [`Pane`] and its elements + /// - the cursor position + fn draw_pane<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style: &Self::Style, + title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>, + body: (&Element<'_, Message, Self>, Layout<'_>), + cursor_position: Point, + ) -> Self::Output; + + /// Draws a [`TitleBar`]. + /// + /// It receives: + /// - the bounds, style of the [`TitleBar`] + /// - the style of the [`TitleBar`] + /// - the title of the [`TitleBar`] with its size, font, and bounds + /// - the controls of the [`TitleBar`] with their [`Layout`+, if any + /// - the cursor position + fn draw_title_bar<Message>( + &mut self, + defaults: &Self::Defaults, + bounds: Rectangle, + style: &Self::Style, + title: &str, + title_size: u16, + title_font: Self::Font, + title_bounds: Rectangle, + controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>, + cursor_position: Point, + ) -> Self::Output; } impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, + Renderer: 'a + self::Renderer + row::Renderer, Message: 'a, { fn from( @@ -665,3 +615,25 @@ where Element::new(pane_grid) } } + +/* + * Helpers + */ +fn hovered_split<'a>( + splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>, + spacing: f32, + cursor_position: Point, +) -> Option<(Split, Axis)> { + splits + .filter_map(|(split, (axis, region, ratio))| { + let bounds = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + if bounds.contains(cursor_position) { + Some((*split, *axis)) + } else { + None + } + }) + .next() +} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index f0e3f362..2320cb7c 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -14,41 +14,225 @@ impl Axis { &self, rectangle: &Rectangle, ratio: f32, - halved_spacing: f32, + spacing: f32, ) -> (Rectangle, Rectangle) { match self { Axis::Horizontal => { - let height_top = (rectangle.height * ratio).round(); - let height_bottom = rectangle.height - height_top; + let height_top = + (rectangle.height * ratio - spacing / 2.0).round(); + let height_bottom = rectangle.height - height_top - spacing; ( Rectangle { - height: height_top - halved_spacing, + height: height_top, ..*rectangle }, Rectangle { - y: rectangle.y + height_top + halved_spacing, - height: height_bottom - halved_spacing, + y: rectangle.y + height_top + spacing, + height: height_bottom, ..*rectangle }, ) } Axis::Vertical => { - let width_left = (rectangle.width * ratio).round(); - let width_right = rectangle.width - width_left; + let width_left = + (rectangle.width * ratio - spacing / 2.0).round(); + let width_right = rectangle.width - width_left - spacing; ( Rectangle { - width: width_left - halved_spacing, + width: width_left, ..*rectangle }, Rectangle { - x: rectangle.x + width_left + halved_spacing, - width: width_right - halved_spacing, + x: rectangle.x + width_left + spacing, + width: width_right, ..*rectangle }, ) } } } + + pub(super) fn split_line_bounds( + &self, + rectangle: Rectangle, + ratio: f32, + spacing: f32, + ) -> Rectangle { + match self { + Axis::Horizontal => Rectangle { + x: rectangle.x, + y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) + .round(), + width: rectangle.width, + height: spacing, + }, + Axis::Vertical => Rectangle { + x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) + .round(), + y: rectangle.y, + width: spacing, + height: rectangle.height, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + enum Case { + Horizontal { + overall_height: f32, + spacing: f32, + top_height: f32, + bottom_y: f32, + bottom_height: f32, + }, + Vertical { + overall_width: f32, + spacing: f32, + left_width: f32, + right_x: f32, + right_width: f32, + }, + } + + #[test] + fn split() { + let cases = vec![ + // Even height, even spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, even spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 2.0, + top_height: 4.0, + bottom_y: 6.0, + bottom_height: 3.0, + }, + // Even height, odd spacing + Case::Horizontal { + overall_height: 10.0, + spacing: 1.0, + top_height: 5.0, + bottom_y: 6.0, + bottom_height: 4.0, + }, + // Odd height, odd spacing + Case::Horizontal { + overall_height: 9.0, + spacing: 1.0, + top_height: 4.0, + bottom_y: 5.0, + bottom_height: 4.0, + }, + // Even width, even spacing + Case::Vertical { + overall_width: 10.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, even spacing + Case::Vertical { + overall_width: 9.0, + spacing: 2.0, + left_width: 4.0, + right_x: 6.0, + right_width: 3.0, + }, + // Even width, odd spacing + Case::Vertical { + overall_width: 10.0, + spacing: 1.0, + left_width: 5.0, + right_x: 6.0, + right_width: 4.0, + }, + // Odd width, odd spacing + Case::Vertical { + overall_width: 9.0, + spacing: 1.0, + left_width: 4.0, + right_x: 5.0, + right_width: 4.0, + }, + ]; + for case in cases { + match case { + Case::Horizontal { + overall_height, + spacing, + top_height, + bottom_y, + bottom_height, + } => { + let a = Axis::Horizontal; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: 10.0, + height: overall_height, + }; + let (top, bottom) = a.split(&r, 0.5, spacing); + assert_eq!( + top, + Rectangle { + height: top_height, + ..r + } + ); + assert_eq!( + bottom, + Rectangle { + y: bottom_y, + height: bottom_height, + ..r + } + ); + } + Case::Vertical { + overall_width, + spacing, + left_width, + right_x, + right_width, + } => { + let a = Axis::Vertical; + let r = Rectangle { + x: 0.0, + y: 0.0, + width: overall_width, + height: 10.0, + }; + let (left, right) = a.split(&r, 0.5, spacing); + assert_eq!( + left, + Rectangle { + width: left_width, + ..r + } + ); + assert_eq!( + right, + Rectangle { + x: right_x, + width: right_width, + ..r + } + ); + } + } + } + } } diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs new file mode 100644 index 00000000..4c43826e --- /dev/null +++ b/native/src/widget/pane_grid/configuration.rs @@ -0,0 +1,26 @@ +use crate::pane_grid::Axis; + +/// The arrangement of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::pane_grid::PaneGrid +#[derive(Debug, Clone)] +pub enum Configuration<T> { + /// A split of the available space. + Split { + /// The direction of the split. + axis: Axis, + + /// The ratio of the split in [0.0, 1.0]. + ratio: f32, + + /// The left/top [`Configuration`] of the split. + a: Box<Configuration<T>>, + + /// The right/bottom [`Configuration`] of the split. + b: Box<Configuration<T>>, + }, + /// A [`Pane`]. + /// + /// [`Pane`]: crate::pane_grid::Pane + Pane(T), +} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8822083e..c9981903 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,30 +1,213 @@ -use crate::pane_grid::Axis; +use crate::container; +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; +use crate::pane_grid::{self, TitleBar}; +use crate::{Clipboard, Element, Hasher, Layout, Point, Size}; -/// The content of a [`PaneGrid`]. +/// The content of a [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -#[derive(Debug, Clone)] -pub enum Content<T> { - /// A split of the available space. - Split { - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Content`] of the split. - /// - /// [`Content`]: enum.Node.html - a: Box<Content<T>>, - - /// The right/bottom [`Content`] of the split. - /// - /// [`Content`]: enum.Node.html - b: Box<Content<T>>, - }, - /// A [`Pane`]. +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct Content<'a, Message, Renderer: pane_grid::Renderer> { + title_bar: Option<TitleBar<'a, Message, Renderer>>, + body: Element<'a, Message, Renderer>, + style: Renderer::Style, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Creates a new [`Content`] with the provided body. + pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { + Self { + title_bar: None, + body: body.into(), + style: Renderer::Style::default(), + } + } + + /// Sets the [`TitleBar`] of this [`Content`]. + pub fn title_bar( + mut self, + title_bar: TitleBar<'a, Message, Renderer>, + ) -> Self { + self.title_bar = Some(title_bar); + self + } + + /// Sets the style of the [`Content`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Pane`]: struct.Pane.html - Pane(T), + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> Renderer::Output { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + let body_layout = children.next().unwrap(); + + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + Some((title_bar, title_bar_layout)), + (&self.body, body_layout), + cursor_position, + ) + } else { + renderer.draw_pane( + defaults, + layout.bounds(), + &self.style, + None, + (&self.body, layout), + cursor_position, + ) + } + } + + /// Returns whether the [`Content`] with the given [`Layout`] can be picked + /// at the provided cursor position. + pub fn can_be_picked_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false + } + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if let Some(title_bar) = &self.title_bar { + let max_size = limits.max(); + + let title_bar_layout = title_bar + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let title_bar_size = title_bar_layout.size(); + + let mut body_layout = self.body.layout( + renderer, + &layout::Limits::new( + Size::ZERO, + Size::new( + max_size.width, + max_size.height - title_bar_size.height, + ), + ), + ); + + body_layout.move_to(Point::new(0.0, title_bar_size.height)); + + layout::Node::with_children( + max_size, + vec![title_bar_layout, body_layout], + ) + } else { + self.body.layout(renderer, limits) + } + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + let mut event_status = event::Status::Ignored; + + let body_layout = if let Some(title_bar) = &mut self.title_bar { + let mut children = layout.children(); + + event_status = title_bar.on_event( + event.clone(), + children.next().unwrap(), + cursor_position, + messages, + renderer, + clipboard, + ); + + children.next().unwrap() + } else { + layout + }; + + let body_status = self.body.on_event( + event, + body_layout, + cursor_position, + messages, + renderer, + clipboard, + ); + + event_status.merge(body_status) + } + + pub(crate) fn hash_layout(&self, state: &mut Hasher) { + if let Some(title_bar) = &self.title_bar { + title_bar.hash_layout(state); + } + + self.body.hash_layout(state); + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let body_layout = if self.title_bar.is_some() { + let mut children = layout.children(); + + // Overlays only allowed in the pane body, for now at least. + let _title_bar_layout = children.next(); + + children.next()? + } else { + layout + }; + + self.body.overlay(body_layout) + } +} + +impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> +where + T: Into<Element<'a, Message, Renderer>>, + Renderer: pane_grid::Renderer + container::Renderer, +{ + fn from(element: T) -> Self { + Self::new(element) + } } diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index 723ec393..319936fc 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -7,17 +7,12 @@ use std::collections::HashMap; /// A layout node of a [`PaneGrid`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone)] pub enum Node { /// The region of this [`Node`] is split into two. - /// - /// [`Node`]: enum.Node.html Split { /// The [`Split`] of this [`Node`]. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html id: Split, /// The direction of the split. @@ -27,28 +22,40 @@ pub enum Node { ratio: f32, /// The left/top [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html a: Box<Node>, /// The right/bottom [`Node`] of the split. - /// - /// [`Node`]: enum.Node.html b: Box<Node>, }, /// The region of this [`Node`] is taken by a [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html Pane(Pane), } impl Node { + /// Returns an iterator over each [`Split`] in this [`Node`]. + pub fn splits(&self) -> impl Iterator<Item = &Split> { + let mut unvisited_nodes = vec![self]; + + std::iter::from_fn(move || { + while let Some(node) = unvisited_nodes.pop() { + match node { + Node::Split { id, a, b, .. } => { + unvisited_nodes.push(a); + unvisited_nodes.push(b); + + return Some(id); + } + _ => {} + } + } + + None + }) + } + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. - /// - /// [`Pane`]: struct.Pane.html - /// [`Node`]: enum.Node.html - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, @@ -56,7 +63,7 @@ impl Node { let mut regions = HashMap::new(); self.compute_regions( - spacing / 2.0, + spacing, &Rectangle { x: 0.0, y: 0.0, @@ -72,10 +79,7 @@ impl Node { /// Returns the axis, rectangular region, and ratio for each [`Split`] in /// the [`Node`] given the spacing between panes and the total available /// space. - /// - /// [`Split`]: struct.Split.html - /// [`Node`]: enum.Node.html - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, @@ -83,7 +87,7 @@ impl Node { let mut splits = HashMap::new(); self.compute_splits( - spacing / 2.0, + spacing, &Rectangle { x: 0.0, y: 0.0, @@ -185,7 +189,7 @@ impl Node { fn compute_regions( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, regions: &mut HashMap<Pane, Rectangle>, ) { @@ -193,11 +197,10 @@ impl Node { Node::Split { axis, ratio, a, b, .. } => { - let (region_a, region_b) = - axis.split(current, *ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); - a.compute_regions(halved_spacing, ®ion_a, regions); - b.compute_regions(halved_spacing, ®ion_b, regions); + a.compute_regions(spacing, ®ion_a, regions); + b.compute_regions(spacing, ®ion_b, regions); } Node::Pane(pane) => { let _ = regions.insert(*pane, *current); @@ -207,7 +210,7 @@ impl Node { fn compute_splits( &self, - halved_spacing: f32, + spacing: f32, current: &Rectangle, splits: &mut HashMap<Split, (Axis, Rectangle, f32)>, ) { @@ -219,13 +222,12 @@ impl Node { b, id, } => { - let (region_a, region_b) = - axis.split(current, *ratio, halved_spacing); + let (region_a, region_b) = axis.split(current, *ratio, spacing); let _ = splits.insert(*id, (*axis, *current, *ratio)); - a.compute_splits(halved_spacing, ®ion_a, splits); - b.compute_splits(halved_spacing, ®ion_b, splits); + a.compute_splits(spacing, ®ion_a, splits); + b.compute_splits(spacing, ®ion_b, splits); } Node::Pane(_) => {} } diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs index f9866407..39d9f3ef 100644 --- a/native/src/widget/pane_grid/pane.rs +++ b/native/src/widget/pane_grid/pane.rs @@ -1,5 +1,5 @@ /// A rectangular region in a [`PaneGrid`] used to display widgets. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs index d020c510..16975abc 100644 --- a/native/src/widget/pane_grid/split.rs +++ b/native/src/widget/pane_grid/split.rs @@ -1,5 +1,5 @@ /// A divider that splits a region in a [`PaneGrid`] into two different panes. /// -/// [`PaneGrid`]: struct.PaneGrid.html +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 4b13fb8e..666e1ca0 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,6 +1,5 @@ use crate::{ - keyboard, - pane_grid::{Axis, Content, Direction, Node, Pane, Split}, + pane_grid::{Axis, Configuration, Direction, Node, Pane, Split}, Hasher, Point, Rectangle, Size, }; @@ -16,32 +15,12 @@ use std::collections::HashMap; /// provided to the view function of [`PaneGrid::new`] for displaying each /// [`Pane`]. /// -/// [`PaneGrid`]: struct.PaneGrid.html -/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new -/// [`Pane`]: struct.Pane.html -/// [`Split`]: struct.Split.html -/// [`State`]: struct.State.html +/// [`PaneGrid`]: crate::widget::PaneGrid +/// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { pub(super) panes: HashMap<Pane, T>, pub(super) internal: Internal, - pub(super) modifiers: keyboard::ModifiersState, -} - -/// The current focus of a [`Pane`]. -/// -/// [`Pane`]: struct.Pane.html -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - /// The [`Pane`] is just focused. - /// - /// [`Pane`]: struct.Pane.html - Idle, - - /// The [`Pane`] is being dragged. - /// - /// [`Pane`]: struct.Pane.html - Dragging, } impl<T> State<T> { @@ -49,113 +28,70 @@ impl<T> State<T> { /// state. /// /// Alongside the [`State`], it returns the first [`Pane`] identifier. - /// - /// [`State`]: struct.State.html - /// [`Pane`]: struct.Pane.html pub fn new(first_pane_state: T) -> (Self, Pane) { - (Self::with_content(Content::Pane(first_pane_state)), Pane(0)) + ( + Self::with_configuration(Configuration::Pane(first_pane_state)), + Pane(0), + ) } - /// Creates a new [`State`] with the given [`Content`]. - /// - /// [`State`]: struct.State.html - /// [`Content`]: enum.Content.html - pub fn with_content(content: impl Into<Content<T>>) -> Self { + /// Creates a new [`State`] with the given [`Configuration`]. + pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); let (layout, last_id) = - Self::distribute_content(&mut panes, content.into(), 0); + Self::distribute_content(&mut panes, config.into(), 0); State { panes, internal: Internal { layout, last_id, - action: Action::Idle { focus: None }, + action: Action::Idle, }, - modifiers: keyboard::ModifiersState::default(), } } /// Returns the total amount of panes in the [`State`]. - /// - /// [`State`]: struct.State.html pub fn len(&self) -> usize { self.panes.len() } /// Returns the internal state of the given [`Pane`], if it exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get(&self, pane: &Pane) -> Option<&T> { self.panes.get(pane) } /// Returns the internal state of the given [`Pane`] with mutability, if it /// exists. - /// - /// [`Pane`]: struct.Pane.html pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { self.panes.get_mut(pane) } /// Returns an iterator over all the panes of the [`State`], alongside its /// internal state. - /// - /// [`State`]: struct.State.html pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> { self.panes.iter() } /// Returns a mutable iterator over all the panes of the [`State`], /// alongside its internal state. - /// - /// [`State`]: struct.State.html pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> { self.panes.iter_mut() } /// Returns the layout of the [`State`]. - /// - /// [`State`]: struct.State.html pub fn layout(&self) -> &Node { &self.internal.layout } - /// Returns the active [`Pane`] of the [`State`], if there is one. - /// - /// A [`Pane`] is active if it is focused and is __not__ being dragged. - /// - /// [`Pane`]: struct.Pane.html - /// [`State`]: struct.State.html - pub fn active(&self) -> Option<Pane> { - self.internal.active_pane() - } - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given /// direction, if there is one. - /// - /// ## Example - /// You can combine this with [`State::active`] to find the pane that is - /// adjacent to the current active one, and then swap them. For instance: - /// - /// ``` - /// # use iced_native::pane_grid; - /// # - /// # let (mut state, _) = pane_grid::State::new(()); - /// # - /// if let Some(active) = state.active() { - /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) { - /// state.swap(&active, &adjacent); - /// } - /// } - /// ``` - /// - /// [`Pane`]: struct.Pane.html - /// [`State::active`]: struct.State.html#method.active pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> { - let regions = - self.internal.layout.regions(0.0, Size::new(4096.0, 4096.0)); + let regions = self + .internal + .layout + .pane_regions(0.0, Size::new(4096.0, 4096.0)); let current_region = regions.get(pane)?; @@ -184,18 +120,8 @@ impl<T> State<T> { Some(*pane) } - /// Focuses the given [`Pane`]. - /// - /// [`Pane`]: struct.Pane.html - pub fn focus(&mut self, pane: &Pane) { - self.internal.focus(pane); - } - /// Splits the given [`Pane`] into two in the given [`Axis`] and /// initializing the new [`Pane`] with the provided internal state. - /// - /// [`Pane`]: struct.Pane.html - /// [`Axis`]: enum.Axis.html pub fn split( &mut self, axis: Axis, @@ -219,7 +145,6 @@ impl<T> State<T> { node.split(new_split, axis, new_pane); let _ = self.panes.insert(new_pane, state); - self.focus(&new_pane); Some((new_pane, new_split)) } @@ -229,9 +154,8 @@ impl<T> State<T> { /// If you want to swap panes on drag and drop in your [`PaneGrid`], you /// will need to call this method when handling a [`DragEvent`]. /// - /// [`State`]: struct.State.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`DragEvent`]: struct.DragEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`DragEvent`]: crate::widget::pane_grid::DragEvent pub fn swap(&mut self, a: &Pane, b: &Pane) { self.internal.layout.update(&|node| match node { Node::Split { .. } => {} @@ -253,20 +177,17 @@ impl<T> State<T> { /// If you want to enable resize interactions in your [`PaneGrid`], you will /// need to call this method when handling a [`ResizeEvent`]. /// - /// [`Split`]: struct.Split.html - /// [`PaneGrid`]: struct.PaneGrid.html - /// [`ResizeEvent`]: struct.ResizeEvent.html + /// [`PaneGrid`]: crate::widget::PaneGrid + /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent pub fn resize(&mut self, split: &Split, ratio: f32) { let _ = self.internal.layout.resize(split, ratio); } - /// Closes the given [`Pane`] and returns its internal state, if it exists. - /// - /// [`Pane`]: struct.Pane.html - pub fn close(&mut self, pane: &Pane) -> Option<T> { + /// Closes the given [`Pane`] and returns its internal state and its closest + /// sibling, if it exists. + pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { if let Some(sibling) = self.internal.layout.remove(pane) { - self.focus(&sibling); - self.panes.remove(pane) + self.panes.remove(pane).map(|state| (state, sibling)) } else { None } @@ -274,11 +195,11 @@ impl<T> State<T> { fn distribute_content( panes: &mut HashMap<Pane, T>, - content: Content<T>, + content: Configuration<T>, next_id: usize, ) -> (Node, usize) { match content { - Content::Split { axis, ratio, a, b } => { + Configuration::Split { axis, ratio, a, b } => { let (a, next_id) = Self::distribute_content(panes, *a, next_id); let (b, next_id) = Self::distribute_content(panes, *b, next_id); @@ -293,7 +214,7 @@ impl<T> State<T> { next_id + 1, ) } - Content::Pane(state) => { + Configuration::Pane(state) => { let id = Pane(next_id); let _ = panes.insert(id, state); @@ -310,47 +231,17 @@ pub struct Internal { action: Action, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - Idle { - focus: Option<Pane>, - }, - Dragging { - pane: Pane, - }, - Resizing { - split: Split, - axis: Axis, - focus: Option<Pane>, - }, -} - -impl Action { - pub fn focus(&self) -> Option<(Pane, Focus)> { - match self { - Action::Idle { focus } | Action::Resizing { focus, .. } => { - focus.map(|pane| (pane, Focus::Idle)) - } - Action::Dragging { pane } => Some((*pane, Focus::Dragging)), - } - } + Idle, + Dragging { pane: Pane, origin: Point }, + Resizing { split: Split, axis: Axis }, } impl Internal { - pub fn action(&self) -> Action { - self.action - } - - pub fn active_pane(&self) -> Option<Pane> { + pub fn picked_pane(&self) -> Option<(Pane, Point)> { match self.action { - Action::Idle { focus } => focus, - _ => None, - } - } - - pub fn picked_pane(&self) -> Option<Pane> { - match self.action { - Action::Dragging { pane } => Some(pane), + Action::Dragging { pane, origin, .. } => Some((pane, origin)), _ => None, } } @@ -362,28 +253,27 @@ impl Internal { } } - pub fn regions( + pub fn pane_regions( &self, spacing: f32, size: Size, ) -> HashMap<Pane, Rectangle> { - self.layout.regions(spacing, size) + self.layout.pane_regions(spacing, size) } - pub fn splits( + pub fn split_regions( &self, spacing: f32, size: Size, ) -> HashMap<Split, (Axis, Rectangle, f32)> { - self.layout.splits(spacing, size) + self.layout.split_regions(spacing, size) } - pub fn focus(&mut self, pane: &Pane) { - self.action = Action::Idle { focus: Some(*pane) }; - } - - pub fn pick_pane(&mut self, pane: &Pane) { - self.action = Action::Dragging { pane: *pane }; + pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { + self.action = Action::Dragging { + pane: *pane, + origin, + }; } pub fn pick_split(&mut self, split: &Split, axis: Axis) { @@ -393,26 +283,14 @@ impl Internal { return; } - let focus = self.action.focus().map(|(pane, _)| pane); - self.action = Action::Resizing { split: *split, axis, - focus, }; } - pub fn drop_split(&mut self) { - match self.action { - Action::Resizing { focus, .. } => { - self.action = Action::Idle { focus }; - } - _ => {} - } - } - - pub fn unfocus(&mut self) { - self.action = Action::Idle { focus: None }; + pub fn idle(&mut self) { + self.action = Action::Idle; } pub fn hash_layout(&self, hasher: &mut Hasher) { diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs new file mode 100644 index 00000000..475cb9ae --- /dev/null +++ b/native/src/widget/pane_grid/title_bar.rs @@ -0,0 +1,253 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::pane_grid; +use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; + +/// The title bar of a [`Pane`]. +/// +/// [`Pane`]: crate::widget::pane_grid::Pane +#[allow(missing_debug_implementations)] +pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { + title: String, + title_size: Option<u16>, + controls: Option<Element<'a, Message, Renderer>>, + padding: u16, + always_show_controls: bool, + style: Renderer::Style, +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Creates a new [`TitleBar`] with the given title. + pub fn new(title: impl Into<String>) -> Self { + Self { + title: title.into(), + title_size: None, + controls: None, + padding: 0, + always_show_controls: false, + style: Renderer::Style::default(), + } + } + + /// Sets the size of the title of the [`TitleBar`]. + pub fn title_size(mut self, size: u16) -> Self { + self.title_size = Some(size); + self + } + + /// Sets the controls of the [`TitleBar`]. + pub fn controls( + mut self, + controls: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.controls = Some(controls.into()); + self + } + + /// Sets the padding of the [`TitleBar`]. + pub fn padding(mut self, units: u16) -> Self { + self.padding = units; + self + } + + /// Sets the style of the [`TitleBar`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } + + /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are + /// always visible. + /// + /// By default, the controls are only visible when the [`Pane`] of this + /// [`TitleBar`] is hovered. + /// + /// [`controls`]: Self::controls + /// [`Pane`]: crate::widget::pane_grid::Pane + pub fn always_show_controls(mut self) -> Self { + self.always_show_controls = true; + self + } +} + +impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> +where + Renderer: pane_grid::Renderer, +{ + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. + /// + /// [`Renderer`]: crate::widget::pane_grid::Renderer + pub fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + show_controls: bool, + ) -> Renderer::Output { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + if let Some(controls) = &self.controls { + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + let controls_layout = children.next().unwrap(); + + let (title_bounds, controls) = + if show_controls || self.always_show_controls { + (title_layout.bounds(), Some((controls, controls_layout))) + } else { + ( + Rectangle { + width: padded.bounds().width, + ..title_layout.bounds() + }, + None, + ) + }; + + renderer.draw_title_bar( + defaults, + layout.bounds(), + &self.style, + &self.title, + self.title_size.unwrap_or(renderer.default_size()), + Renderer::Font::default(), + title_bounds, + controls, + cursor_position, + ) + } else { + renderer.draw_title_bar::<()>( + defaults, + layout.bounds(), + &self.style, + &self.title, + self.title_size.unwrap_or(renderer.default_size()), + Renderer::Font::default(), + padded.bounds(), + None, + cursor_position, + ) + } + } + + /// Returns whether the mouse cursor is over the pick area of the + /// [`TitleBar`] or not. + /// + /// The whole [`TitleBar`] is a pick area, except its controls. + pub fn is_over_pick_area( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if layout.bounds().contains(cursor_position) { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + if self.controls.is_some() { + let mut children = padded.children(); + let _ = children.next().unwrap(); + let controls_layout = children.next().unwrap(); + + !controls_layout.bounds().contains(cursor_position) + } else { + true + } + } else { + false + } + } + + pub(crate) fn hash_layout(&self, hasher: &mut Hasher) { + use std::hash::Hash; + + self.title.hash(hasher); + self.title_size.hash(hasher); + self.padding.hash(hasher); + } + + pub(crate) fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let padding = f32::from(self.padding); + let limits = limits.pad(padding); + let max_size = limits.max(); + + let title_size = self.title_size.unwrap_or(renderer.default_size()); + let title_font = Renderer::Font::default(); + + let (title_width, title_height) = renderer.measure( + &self.title, + title_size, + title_font, + Size::new(f32::INFINITY, max_size.height), + ); + + let mut node = if let Some(controls) = &self.controls { + let mut controls_layout = controls + .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + + let controls_size = controls_layout.size(); + let space_before_controls = max_size.width - controls_size.width; + + let mut title_layout = layout::Node::new(Size::new( + title_width.min(space_before_controls), + title_height, + )); + + let title_size = title_layout.size(); + let height = title_size.height.max(controls_size.height); + + title_layout + .move_to(Point::new(0.0, (height - title_size.height) / 2.0)); + controls_layout.move_to(Point::new(space_before_controls, 0.0)); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![title_layout, controls_layout], + ) + } else { + layout::Node::new(Size::new(max_size.width, title_height)) + }; + + node.move_to(Point::new(padding, padding)); + + layout::Node::with_children(node.size().pad(padding), vec![node]) + } + + pub(crate) fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + renderer: &Renderer, + clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + if let Some(controls) = &mut self.controls { + let mut children = layout.children(); + let padded = children.next().unwrap(); + + let mut children = padded.children(); + let _ = children.next(); + let controls_layout = children.next().unwrap(); + + controls.on_event( + event, + controls_layout, + cursor_position, + messages, + renderer, + clipboard, + ) + } else { + event::Status::Ignored + } + } +} diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs new file mode 100644 index 00000000..58c0dfe1 --- /dev/null +++ b/native/src/widget/pick_list.rs @@ -0,0 +1,343 @@ +//! Display a dropdown list of selectable values. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::overlay::menu::{self, Menu}; +use crate::scrollable; +use crate::text; +use crate::{ + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; +use std::borrow::Cow; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: self::Renderer> +where + [T]: ToOwned<Owned = Vec<T>>, +{ + menu: &'a mut menu::State, + is_open: &'a mut bool, + hovered_option: &'a mut Option<usize>, + last_selection: &'a mut Option<T>, + on_selected: Box<dyn Fn(T) -> Message>, + options: Cow<'a, [T]>, + selected: Option<T>, + width: Length, + padding: u16, + text_size: Option<u16>, + font: Renderer::Font, + style: <Renderer as self::Renderer>::Style, +} + +/// The local state of a [`PickList`]. +#[derive(Debug, Clone)] +pub struct State<T> { + menu: menu::State, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> Default for State<T> { + fn default() -> Self { + Self { + menu: menu::State::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl<'a, T: 'a, Message, Renderer: self::Renderer> + PickList<'a, T, Message, Renderer> +where + T: ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, +{ + /// Creates a new [`PickList`] with the given [`State`], a list of options, + /// the current selected value, and the message to produce when an option is + /// selected. + pub fn new( + state: &'a mut State<T>, + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'static, + ) -> Self { + let State { + menu, + is_open, + hovered_option, + last_selection, + } = state; + + Self { + menu, + is_open, + hovered_option, + last_selection, + on_selected: Box::new(on_selected), + options: options.into(), + selected, + width: Length::Shrink, + text_size: None, + padding: Renderer::DEFAULT_PADDING, + font: Default::default(), + style: Default::default(), + } + } + + /// Sets the width of the [`PickList`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the padding of the [`PickList`]. + pub fn padding(mut self, padding: u16) -> Self { + self.padding = padding; + self + } + + /// Sets the text size of the [`PickList`]. + pub fn text_size(mut self, size: u16) -> Self { + self.text_size = Some(size); + self + } + + /// Sets the font of the [`PickList`]. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`PickList`]. + pub fn style( + mut self, + style: impl Into<<Renderer as self::Renderer>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: self::Renderer + scrollable::Renderer + 'a, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + use std::f32; + + let limits = limits + .width(self.width) + .height(Length::Shrink) + .pad(f32::from(self.padding)); + + let text_size = self.text_size.unwrap_or(renderer.default_size()); + + let max_width = match self.width { + Length::Shrink => { + let labels = self.options.iter().map(ToString::to_string); + + labels + .map(|label| { + let (width, _) = renderer.measure( + &label, + text_size, + Renderer::Font::default(), + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }) + .max() + .unwrap_or(100) + } + _ => 0, + }; + + let size = { + let intrinsic = Size::new( + max_width as f32 + + f32::from(text_size) + + f32::from(self.padding), + f32::from(text_size), + ); + + limits.resolve(intrinsic).pad(f32::from(self.padding)) + }; + + layout::Node::new(size) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash as _; + + match self.width { + Length::Shrink => { + self.options + .iter() + .map(ToString::to_string) + .for_each(|label| label.hash(state)); + } + _ => { + self.width.hash(state); + } + } + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec<Message>, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let event_status = if *self.is_open { + // TODO: Encode cursor availability in the type system + *self.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + let selected = self.selected.as_ref(); + + *self.is_open = true; + *self.hovered_option = self + .options + .iter() + .position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = self.last_selection.take() { + messages.push((self.on_selected)(last_selection)); + + *self.is_open = false; + + event::Status::Captured + } else { + event_status + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + _defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + self::Renderer::draw( + renderer, + layout.bounds(), + cursor_position, + self.selected.as_ref().map(ToString::to_string), + self.padding, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + &self.style, + ) + } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + if *self.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut self.menu, + &self.options, + &mut self.hovered_option, + &mut self.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(self.padding) + .font(self.font) + .style(Renderer::menu_style(&self.style)); + + if let Some(text_size) = self.text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } + } +} + +/// The renderer of a [`PickList`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PickList`] in your user interface. +/// +/// [renderer]: crate::renderer +pub trait Renderer: text::Renderer + menu::Renderer { + /// The default padding of a [`PickList`]. + const DEFAULT_PADDING: u16; + + /// The [`PickList`] style supported by this renderer. + type Style: Default; + + /// Returns the style of the [`Menu`] of the [`PickList`]. + fn menu_style( + style: &<Self as Renderer>::Style, + ) -> <Self as menu::Renderer>::Style; + + /// Draws a [`PickList`]. + fn draw( + &mut self, + bounds: Rectangle, + cursor_position: Point, + selected: Option<String>, + padding: u16, + text_size: u16, + font: Self::Font, + style: &<Self as Renderer>::Style, + ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: self::Renderer + 'a, + Message: 'static, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 5ab76d47..d294f198 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { /// 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()), @@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> { } /// 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 @@ -104,6 +96,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw( layout.bounds(), @@ -127,15 +120,12 @@ where /// 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 +/// [renderer]: crate::renderer 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`]. @@ -146,8 +136,6 @@ pub trait Renderer: crate::Renderer { /// * 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, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index d07a9012..69952345 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,8 +1,13 @@ //! Create choices using radio buttons. +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::row; +use crate::text; +use crate::touch; use crate::{ - layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, - HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, - VerticalAlignment, Widget, + Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, + Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; use std::hash::Hash; @@ -41,12 +46,14 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { width: Length, size: u16, spacing: u16, - text_size: u16, + text_size: Option<u16>, style: Renderer::Style, } impl<Message, Renderer: self::Renderer + text::Renderer> Radio<Message, Renderer> +where + Message: Clone, { /// Creates a new [`Radio`] button. /// @@ -56,8 +63,6 @@ impl<Message, Renderer: self::Renderer + text::Renderer> /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It /// receives the value of the radio and must produce a `Message`. - /// - /// [`Radio`]: struct.Radio.html pub fn new<F, V>( value: V, label: impl Into<String>, @@ -75,46 +80,36 @@ impl<Message, Renderer: self::Renderer + text::Renderer> width: Length::Shrink, size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 - text_size: <Renderer as text::Renderer>::DEFAULT_SIZE, + text_size: None, style: Renderer::Style::default(), } } /// Sets the size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn size(mut self, size: u16) -> Self { self.size = size; self } /// Sets the width of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the spacing between the [`Radio`] button and the text. - /// - /// [`Radio`]: struct.Radio.html pub fn spacing(mut self, spacing: u16) -> Self { self.spacing = spacing; self } /// Sets the text size of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn text_size(mut self, text_size: u16) -> Self { - self.text_size = text_size; + self.text_size = Some(text_size); self } /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self @@ -123,8 +118,8 @@ impl<Message, Renderer: self::Renderer + text::Renderer> impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where - Renderer: self::Renderer + text::Renderer + row::Renderer, Message: Clone, + Renderer: self::Renderer + text::Renderer + row::Renderer, { fn width(&self) -> Length { self.width @@ -151,7 +146,7 @@ where .push( Text::new(&self.label) .width(self.width) - .size(self.text_size), + .size(self.text_size.unwrap_or(renderer.default_size())), ) .layout(renderer, limits) } @@ -164,15 +159,20 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); + + return event::Status::Captured; } } _ => {} } + + event::Status::Ignored } fn draw( @@ -181,6 +181,7 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let mut children = layout.children(); @@ -194,7 +195,7 @@ where defaults, label_layout.bounds(), &self.label, - self.text_size, + self.text_size.unwrap_or(renderer.default_size()), Default::default(), None, HorizontalAlignment::Left, @@ -226,20 +227,15 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Radio`] button in your user interface. /// -/// [`Radio`]: struct.Radio.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; /// The default size of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SIZE: u16; /// The default spacing of a [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html const DEFAULT_SPACING: u16; /// Draws a [`Radio`] button. @@ -249,8 +245,6 @@ pub trait Renderer: crate::Renderer { /// * whether the [`Radio`] is selected or not /// * whether the mouse is over the [`Radio`] or not /// * the drawn label of the [`Radio`] - /// - /// [`Radio`]: struct.Radio.html fn draw( &mut self, bounds: Rectangle, @@ -264,8 +258,8 @@ pub trait Renderer: crate::Renderer { impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer + row::Renderer + text::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer + row::Renderer + text::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 31f7472f..b71663bd 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,18 +1,15 @@ //! Distribute content horizontally. -use std::hash::Hash; - +use crate::event::{self, Event}; +use crate::layout; +use crate::overlay; use crate::{ - layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, }; +use std::hash::Hash; use std::u32; /// A container that distributes its contents horizontally. -/// -/// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, @@ -27,15 +24,11 @@ pub struct Row<'a, Message, Renderer> { impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children( children: Vec<Element<'a, Message, Renderer>>, ) -> Self { @@ -62,57 +55,42 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -165,9 +143,11 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { child.widget.on_event( event.clone(), layout, @@ -176,8 +156,8 @@ where renderer, clipboard, ) - }, - ); + }) + .fold(event::Status::Ignored, event::Status::merge) } fn draw( @@ -186,8 +166,15 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Renderer::Output { - renderer.draw(defaults, &self.children, layout, cursor_position) + renderer.draw( + defaults, + &self.children, + layout, + cursor_position, + viewport, + ) } fn hash_layout(&self, state: &mut Hasher) { @@ -200,12 +187,23 @@ where self.max_height.hash(state); self.align_items.hash(state); self.spacing.hash(state); - self.spacing.hash(state); + self.padding.hash(state); for child in &self.children { child.widget.hash_layout(state); } } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.children + .iter_mut() + .zip(layout.children()) + .filter_map(|(child, layout)| child.widget.overlay(layout)) + .next() + } } /// The renderer of a [`Row`]. @@ -213,8 +211,7 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Row`] in your user interface. /// -/// [`Row`]: struct.Row.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// Draws a [`Row`]. /// @@ -222,15 +219,13 @@ pub trait Renderer: crate::Renderer + Sized { /// - the children of the [`Row`] /// - the [`Layout`] of the [`Row`] and its children /// - the cursor position - /// - /// [`Row`]: struct.Row.html - /// [`Layout`]: ../layout/struct.Layout.html fn draw<Message>( &mut self, defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, + viewport: &Rectangle, ) -> Self::Output; } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs new file mode 100644 index 00000000..18c88658 --- /dev/null +++ b/native/src/widget/rule.rs @@ -0,0 +1,116 @@ +//! Display a horizontal or vertical rule for dividing content. + +use std::hash::Hash; + +use crate::{ + layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, +}; + +/// Display a horizontal or vertical rule for dividing content. +#[derive(Debug, Copy, Clone)] +pub struct Rule<Renderer: self::Renderer> { + width: Length, + height: Length, + style: Renderer::Style, + is_horizontal: bool, +} + +impl<Renderer: self::Renderer> Rule<Renderer> { + /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. + pub fn horizontal(spacing: u16) -> Self { + Rule { + width: Length::Fill, + height: Length::from(Length::Units(spacing)), + style: Renderer::Style::default(), + is_horizontal: true, + } + } + + /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. + pub fn vertical(spacing: u16) -> Self { + Rule { + width: Length::from(Length::Units(spacing)), + height: Length::Fill, + style: Renderer::Style::default(), + is_horizontal: false, + } + } + + /// Sets the style of the [`Rule`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> +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, + _viewport: &Rectangle, + ) -> Renderer::Output { + renderer.draw(layout.bounds(), &self.style, self.is_horizontal) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +/// The renderer of a [`Rule`]. +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// Draws a [`Rule`]. + /// + /// It receives: + /// * the bounds of the [`Rule`] + /// * the style of the [`Rule`] + /// * whether the [`Rule`] is horizontal (true) or vertical (false) + fn draw( + &mut self, + bounds: Rectangle, + style: &Self::Style, + is_horizontal: bool, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Rule<Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer, + Message: 'a, +{ + fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { + Element::new(rule) + } +} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 3c8e5e5b..18cdf169 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,13 @@ //! Navigate an endless amount of content with a scrollbar. +use crate::column; +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::touch; use crate::{ - column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, - Layout, Length, Point, Rectangle, Size, Widget, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, + Rectangle, Size, Vector, Widget, }; use std::{f32, hash::Hash, u32}; @@ -13,20 +19,23 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { state: &'a mut State, height: Length, max_height: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, content: Column<'a, Message, Renderer>, style: Renderer::Style, } impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(state: &'a mut State) -> Self { Scrollable { state, height: Length::Shrink, max_height: u32::MAX, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, content: Column::new(), style: Renderer::Style::default(), } @@ -43,64 +52,68 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn padding(mut self, units: u16) -> Self { self.content = self.content.padding(units); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.content = self.content.width(width); self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); self } + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { + self.scrollbar_width = scrollbar_width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { + self.scrollbar_margin = scrollbar_margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// Silently enforces a minimum value of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } + /// Sets 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 pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message, Renderer>>, @@ -113,7 +126,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { impl<'a, Message, Renderer> Widget<Message, Renderer> for Scrollable<'a, Message, Renderer> where - Renderer: self::Renderer + column::Renderer, + Renderer: self::Renderer, { fn width(&self) -> Length { Widget::<Message, Renderer>::width(&self.content) @@ -152,14 +165,56 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - // TODO: Event capture. Nested scrollables should capture scroll events. + let offset = self.state.offset(bounds, content_bounds); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + + self.state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) + }; + + self.content.on_event( + event.clone(), + content, + cursor_position, + messages, + renderer, + clipboard, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + if is_mouse_over { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { @@ -172,26 +227,57 @@ where self.state.scroll(y, bounds, content_bounds); } } + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + self.state.scroll_box_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + self.state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; + + self.state.scroll( + delta, + bounds, + content_bounds, + ); + + self.state.scroll_box_touched_at = + Some(cursor_position); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + self.state.scroll_box_touched_at = None; + } + } + + return event::Status::Captured; } _ => {} } } - 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::ButtonReleased( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.scroller_grabbed_at = None; + + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { if let (Some(scrollbar), Some(scroller_grabbed_at)) = (scrollbar, self.state.scroller_grabbed_at) { @@ -203,6 +289,8 @@ where bounds, content_bounds, ); + + return event::Status::Captured; } } _ => {} @@ -211,7 +299,8 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, - )) => { + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) @@ -227,6 +316,8 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + + return event::Status::Captured; } } } @@ -234,28 +325,7 @@ where } } - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event, - content, - cursor_position, - messages, - renderer, - clipboard, - ) + event::Status::Ignored } fn draw( @@ -264,12 +334,20 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); let offset = self.state.offset(bounds, content_bounds); - let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); + let scrollbar = renderer.scrollbar( + bounds, + content_bounds, + offset, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + ); let is_mouse_over = bounds.contains(cursor_position); let is_mouse_over_scrollbar = scrollbar @@ -289,6 +367,10 @@ where defaults, content_layout, cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, ) }; @@ -315,30 +397,42 @@ where self.content.hash_layout(state) } + + fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let Self { content, state, .. } = self; + + content + .overlay(layout.children().next().unwrap()) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug, Clone, Copy, Default)] pub struct State { scroller_grabbed_at: Option<f32>, + scroll_box_touched_at: Option<Point>, offset: f32, } impl State { /// Creates a new [`State`] with the scrollbar located at the top. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } /// Apply a scrolling offset to the current [`State`], given the bounds of /// the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll( &mut self, delta_y: f32, @@ -359,9 +453,6 @@ impl State { /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn scroll_to( &mut self, percentage: f32, @@ -374,9 +465,6 @@ impl State { /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { let hidden_content = (content_bounds.height - bounds.height).max(0.0).round() as u32; @@ -388,31 +476,37 @@ impl State { pub fn is_scroller_grabbed(&self) -> bool { self.scroller_grabbed_at.is_some() } + + /// Returns whether the scroll box is currently touched or not. + pub fn is_scroll_box_touched(&self) -> bool { + self.scroll_box_touched_at.is_some() + } } /// The scrollbar of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug)] pub struct Scrollbar { + /// The outer bounds of the scrollable, including the [`Scrollbar`] and + /// [`Scroller`]. + pub outer_bounds: Rectangle, + /// The bounds of the [`Scrollbar`]. - /// - /// [`Scrollbar`]: struct.Scrollbar.html pub bounds: Rectangle, + /// The margin within the [`Scrollbar`]. + pub margin: u16, + /// 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) + self.outer_bounds.contains(cursor_position) } fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { - if self.bounds.contains(cursor_position) { + if self.outer_bounds.contains(cursor_position) { Some(if self.scroller.bounds.contains(cursor_position) { (cursor_position.y - self.scroller.bounds.y) / self.scroller.bounds.height @@ -437,13 +531,9 @@ impl Scrollbar { } /// 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, } @@ -452,22 +542,21 @@ pub struct Scroller { /// Your [renderer] will need to implement this trait before being /// able to use a [`Scrollable`] in your user interface. /// -/// [`Scrollable`]: struct.Scrollable.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: column::Renderer + Sized { /// 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 scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, offset: u32, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, ) -> Option<Scrollbar>; /// Draws the [`Scrollable`]. @@ -481,10 +570,6 @@ pub trait Renderer: crate::Renderer + Sized { /// - 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( &mut self, scrollable: &State, @@ -502,7 +587,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: 'a + self::Renderer, Message: 'a, { fn from( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 8cdfc3de..010c6e53 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -1,12 +1,12 @@ //! 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::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::touch; use crate::{ - layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::{hash::Hash, ops::RangeInclusive}; @@ -16,13 +16,15 @@ use std::{hash::Hash, ops::RangeInclusive}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// -/// [`Slider`]: struct.Slider.html +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. /// /// # Example /// ``` /// # use iced_native::{slider, renderer::Null}; /// # -/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>; +/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>; +/// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } @@ -35,16 +37,24 @@ use std::{hash::Hash, ops::RangeInclusive}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message, Renderer: self::Renderer> { +pub struct Slider<'a, T, Message, Renderer: self::Renderer> { state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Box<dyn Fn(f32) -> Message>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Box<dyn Fn(T) -> Message>, + on_release: Option<Message>, width: Length, + height: u16, style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -54,48 +64,77 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> { /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Box::new(on_change), + on_release: None, width: Length::Fill, + height: Renderer::DEFAULT_HEIGHT, style: Renderer::Style::default(), } } - /// Sets the width of the [`Slider`]. + /// Sets the release message of the [`Slider`]. + /// This is called when the mouse is released from the slider. /// - /// [`Slider`]: struct.Slider.html + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`Slider`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } + /// Sets the height of the [`Slider`]. + pub fn height(mut self, height: u16) -> Self { + self.height = height; + self + } + /// Sets the style of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); self } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { is_dragging: bool, @@ -103,16 +142,16 @@ pub struct State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, Message, Renderer> +impl<'a, T, Message, Renderer> Widget<Message, Renderer> + for Slider<'a, T, Message, Renderer> where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, Renderer: self::Renderer, { fn width(&self) -> Length { @@ -125,12 +164,11 @@ where fn layout( &self, - renderer: &Renderer, + _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(Length::Units(renderer.height() as u16)); + let limits = + limits.width(self.width).height(Length::Units(self.height)); let size = limits.resolve(Size::ZERO); @@ -145,43 +183,64 @@ where messages: &mut Vec<Message>, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { let mut change = || { let bounds = layout.bounds(); - if cursor_position.x <= bounds.x { messages.push((self.on_change)(*self.range.start())); } else if cursor_position.x >= bounds.x + bounds.width { messages.push((self.on_change)(*self.range.end())); } else { - let percent = (cursor_position.x - bounds.x) / bounds.width; - let value = (self.range.end() - self.range.start()) * percent - + self.range.start(); + let step = self.step.into(); + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); - messages.push((self.on_change)(value)); + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + messages.push((self.on_change)(value)); + } } }; match event { - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + self.state.is_dragging = true; + + return event::Status::Captured; } - mouse::Event::ButtonReleased(mouse::Button::Left) => { + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if self.state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + messages.push(on_release); + } self.state.is_dragging = false; + + return event::Status::Captured; } - mouse::Event::CursorMoved { .. } => { - if self.state.is_dragging { - change(); - } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if self.state.is_dragging { + change(); + + return event::Status::Captured; } - _ => {} - }, + } _ => {} } + + event::Status::Ignored } fn draw( @@ -190,12 +249,16 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { + let start = *self.range.start(); + let end = *self.range.end(); + renderer.draw( layout.bounds(), cursor_position, - self.range.clone(), - self.value, + start.into() as f32..=end.into() as f32, + self.value.into() as f32, self.state.is_dragging, &self.style, ) @@ -214,16 +277,13 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`Slider`] in your user interface. /// -/// [`Slider`]: struct.Slider.html -/// [renderer]: ../../renderer/index.html +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// The style supported by this renderer. type Style: Default; - /// Returns the height of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html - fn height(&self) -> u32; + /// The default height of a [`Slider`]. + const DEFAULT_HEIGHT: u16; /// Draws a [`Slider`]. /// @@ -233,10 +293,6 @@ pub trait Renderer: crate::Renderer { /// * the local state of the [`Slider`] /// * the range of values of the [`Slider`] /// * the current value of the [`Slider`] - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html - /// [`Class`]: enum.Class.html fn draw( &mut self, bounds: Rectangle, @@ -248,14 +304,15 @@ pub trait Renderer: crate::Renderer { ) -> Self::Output; } -impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where + T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, + Message: 'a + Clone, Renderer: 'a + self::Renderer, - Message: 'a, { fn from( - slider: Slider<'a, Message, Renderer>, + slider: Slider<'a, T, Message, Renderer>, ) -> Element<'a, Message, Renderer> { Element::new(slider) } diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index e56a8fe1..6b34ece8 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -16,15 +16,11 @@ pub struct Space { 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, @@ -33,8 +29,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, @@ -43,7 +37,7 @@ impl Space { } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: self::Renderer, { @@ -71,6 +65,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(layout.bounds()) } @@ -84,14 +79,10 @@ where } /// 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; } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 114d5e41..9cd61918 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,5 +1,6 @@ //! Display vector graphics in your application. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget}; use std::{ hash::{Hash, Hasher as _}, @@ -13,8 +14,6 @@ use std::{ /// /// [`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, @@ -24,9 +23,6 @@ pub struct Svg { 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(), @@ -37,23 +33,17 @@ impl Svg { /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. - /// - /// [`Svg`]: struct.Svg.html pub fn from_path(path: impl Into<PathBuf>) -> Self { Self::new(Handle::from_path(path)) } /// 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 @@ -103,6 +93,7 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { renderer.draw(self.handle.clone(), layout) } @@ -117,8 +108,6 @@ where } /// An [`Svg`] handle. -/// -/// [`Svg`]: struct.Svg.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -128,8 +117,6 @@ pub struct Handle { impl Handle { /// Creates an SVG [`Handle`] pointing to the vector image of the given /// path. - /// - /// [`Handle`]: struct.Handle.html pub fn from_path(path: impl Into<PathBuf>) -> Handle { Self::from_data(Data::Path(path.into())) } @@ -139,8 +126,6 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - /// - /// [`Handle`]: struct.Handle.html pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -156,15 +141,11 @@ impl Handle { } /// Returns the unique identifier of the [`Handle`]. - /// - /// [`Handle`]: struct.Handle.html pub fn id(&self) -> u64 { self.id } /// Returns a reference to the SVG [`Data`]. - /// - /// [`Data`]: enum.Data.html pub fn data(&self) -> &Data { &self.data } @@ -177,8 +158,6 @@ impl Hash for Handle { } /// The data of an [`Svg`]. -/// -/// [`Svg`]: struct.Svg.html #[derive(Clone, Hash)] pub enum Data { /// File data @@ -204,18 +183,12 @@ impl std::fmt::Debug for Data { /// 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 +/// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an [`Svg`] for the given [`Handle`]. - /// - /// [`Svg`]: struct.Svg.html - /// [`Handle`]: struct.Handle.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; } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index d60aa468..6cc18e6c 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -19,7 +19,7 @@ use std::hash::Hash; /// ``` /// ///  -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Text<Renderer: self::Renderer> { content: String, size: Option<u16>, @@ -33,8 +33,6 @@ pub struct Text<Renderer: self::Renderer> { impl<Renderer: self::Renderer> Text<Renderer> { /// Create a new fragment of [`Text`] with the given contents. - /// - /// [`Text`]: struct.Text.html pub fn new<T: Into<String>>(label: T) -> Self { Text { content: label.into(), @@ -49,17 +47,12 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the size of the [`Text`]. - /// - /// [`Text`]: struct.Text.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self } /// Sets the [`Color`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`Color`]: ../../struct.Color.html pub fn color<C: Into<Color>>(mut self, color: C) -> Self { self.color = Some(color.into()); self @@ -67,33 +60,25 @@ impl<Renderer: self::Renderer> Text<Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html + /// [`Font`]: Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = font.into(); self } /// Sets the width of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the [`HorizontalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html pub fn horizontal_alignment( mut self, alignment: HorizontalAlignment, @@ -103,9 +88,6 @@ impl<Renderer: self::Renderer> Text<Renderer> { } /// Sets the [`VerticalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self { self.vertical_alignment = alignment; self @@ -131,7 +113,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(); @@ -149,12 +131,13 @@ where defaults: &Renderer::Defaults, layout: Layout<'_>, _cursor_position: Point, + _viewport: &Rectangle, ) -> 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, @@ -176,26 +159,18 @@ where /// The renderer of a [`Text`] fragment. /// /// Your [renderer] will need to implement this trait before being -/// able to use [`Text`] in your [`UserInterface`]. +/// able to use [`Text`] in your user interface. /// -/// [`Text`]: struct.Text.html -/// [renderer]: ../../renderer/index.html -/// [`UserInterface`]: ../../struct.UserInterface.html +/// [renderer]: crate::Renderer pub trait Renderer: crate::Renderer { /// The font type used for [`Text`]. - /// - /// [`Text`]: struct.Text.html type Font: Default + Copy; - /// The default size of [`Text`]. - /// - /// [`Text`]: struct.Text.html - const DEFAULT_SIZE: u16; + /// Returns the default size of [`Text`]. + fn default_size(&self) -> u16; /// Measures the [`Text`] in the given bounds and returns the minimum /// boundaries that can fit the contents. - /// - /// [`Text`]: struct.Text.html fn measure( &self, content: &str, @@ -213,10 +188,6 @@ pub trait Renderer: crate::Renderer { /// * the color of the [`Text`] /// * the [`HorizontalAlignment`] of the [`Text`] /// * the [`VerticalAlignment`] of the [`Text`] - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html fn draw( &mut self, defaults: &Self::Defaults, @@ -239,3 +210,18 @@ where Element::new(text) } } + +impl<Renderer: self::Renderer> Clone for Text<Renderer> { + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + color: self.color, + font: self.font, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + } + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 1cdbe007..2fd9cec1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1,9 +1,6 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -//! -//! [`TextInput`]: struct.TextInput.html -//! [`State`]: struct.State.html mod editor; mod value; @@ -14,11 +11,14 @@ pub use value::Value; use editor::Editor; +use crate::event::{self, Event}; +use crate::keyboard; +use crate::layout; +use crate::mouse::{self, click}; +use crate::text; +use crate::touch; use crate::{ - keyboard, layout, - mouse::{self, click}, - Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, - Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::u32; @@ -53,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { placeholder: String, value: Value, is_secure: bool, - font: Font, + font: Renderer::Font, width: Length, max_width: u32, padding: u16, @@ -63,7 +63,11 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { style: Renderer::Style, } -impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: self::Renderer, +{ /// Creates a new [`TextInput`]. /// /// It expects: @@ -71,9 +75,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -88,7 +89,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, - font: Font::Default, + font: Default::default(), width: Length::Fill, max_width: u32::MAX, padding: 0, @@ -100,8 +101,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { } /// Converts the [`TextInput`] into a secure password input. - /// - /// [`TextInput`]: struct.TextInput.html pub fn password(mut self) -> Self { self.is_secure = true; self @@ -109,39 +108,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the [`Font`] of the [`Text`]. /// - /// [`Text`]: struct.Text.html - /// [`Font`]: ../../struct.Font.html - pub fn font(mut self, font: Font) -> Self { + /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Text`]: crate::widget::Text + pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -149,27 +140,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { 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 } + + /// Returns the current [`State`] of the [`TextInput`]. + pub fn state(&self) -> &State { + self.state + } +} + +impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +where + Renderer: self::Renderer, +{ + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its + /// [`Value`] if provided. + pub fn draw( + &self, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + value: Option<&Value>, + ) -> Renderer::Output { + let value = value.unwrap_or(&self.value); + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + if self.is_secure { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + &value.secure(), + &self.state, + &self.style, + ) + } else { + self::Renderer::draw( + renderer, + bounds, + text_bounds, + cursor_position, + self.font, + self.size.unwrap_or(renderer.default_size()), + &self.placeholder, + value, + &self.state, + &self.style, + ) + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> for TextInput<'a, Message, Renderer> where - Renderer: self::Renderer, Message: Clone, + Renderer: self::Renderer, { fn width(&self) -> Length { self.width @@ -207,11 +246,14 @@ where messages: &mut Vec<Message>, renderer: &Renderer, clipboard: Option<&dyn Clipboard>, - ) { + ) -> event::Status { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let is_clicked = layout.bounds().contains(cursor_position); + self.state.is_focused = is_clicked; + if is_clicked { let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; @@ -243,6 +285,8 @@ where } else { self.state.cursor.move_to(0); } + + self.state.is_dragging = true; } click::Kind::Double => { if self.is_secure { @@ -262,25 +306,30 @@ where self.value.next_end_of_word(position), ); } + + self.state.is_dragging = false; } click::Kind::Triple => { self.state.cursor.select_all(&self.value); + self.state.is_dragging = false; } } self.state.last_click = Some(click); - } - self.state.is_dragging = is_clicked; - self.state.is_focused = is_clicked; + return event::Status::Captured; + } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { self.state.is_dragging = false; } - Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { if self.state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = x - text_layout.bounds().x; + let target = position.x - text_layout.bounds().x; if target > 0.0 { let value = if self.is_secure { @@ -303,11 +352,14 @@ where position, ); } + + return event::Status::Captured; } } Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() + && !self.state.keyboard_modifiers.is_command_pressed() && !c.is_control() => { let mut editor = @@ -317,162 +369,212 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); + + return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { - key_code, - modifiers, - }) if self.state.is_focused => match key_code { - keyboard::KeyCode::Enter => { - if let Some(on_submit) = self.on_submit.clone() { - messages.push(on_submit); - } - } - keyboard::KeyCode::Backspace => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection().is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); - } else { - self.state.cursor.select_left_by_words(&self.value); + key_code, .. + }) if self.state.is_focused => { + let modifiers = self.state.keyboard_modifiers; + + match key_code { + keyboard::KeyCode::Enter => { + if let Some(on_submit) = self.on_submit.clone() { + messages.push(on_submit); } } + keyboard::KeyCode::Backspace => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state + .cursor + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state.cursor.select_range(0, cursor_pos); + } else { + self.state + .cursor + .select_left_by_words(&self.value); + } + } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.backspace(); + editor.backspace(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Delete => { - if platform::is_jump_modifier_pressed(modifiers) - && self.state.cursor.selection().is_none() - { - if self.is_secure { - let cursor_pos = self.state.cursor.end(&self.value); - self.state - .cursor - .select_range(cursor_pos, self.value.len()); - } else { - self.state + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Delete => { + if platform::is_jump_modifier_pressed(modifiers) + && self + .state .cursor - .select_right_by_words(&self.value); + .selection(&self.value) + .is_none() + { + if self.is_secure { + let cursor_pos = + self.state.cursor.end(&self.value); + self.state + .cursor + .select_range(cursor_pos, self.value.len()); + } else { + self.state + .cursor + .select_right_by_words(&self.value); + } } - } - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - editor.delete(); + editor.delete(); - let message = (self.on_change)(editor.contents()); - messages.push(message); - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state.cursor.select_left_by_words(&self.value); + let message = (self.on_change)(editor.contents()); + messages.push(message); + } + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift { + self.state + .cursor + .select_left_by_words(&self.value); + } else { + self.state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_left(&self.value) } else { - self.state.cursor.move_left_by_words(&self.value); + self.state.cursor.move_left(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_left(&self.value) - } else { - self.state.cursor.move_left(&self.value); } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { - if modifiers.shift { - self.state - .cursor - .select_right_by_words(&self.value); + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift { + self.state + .cursor + .select_right_by_words(&self.value); + } else { + self.state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift { + self.state.cursor.select_right(&self.value) } else { - self.state.cursor.move_right_by_words(&self.value); + self.state.cursor.move_right(&self.value); } - } else if modifiers.shift { - self.state.cursor.select_right(&self.value) - } else { - self.state.cursor.move_right(&self.value); } - } - keyboard::KeyCode::Home => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); - } else { - self.state.cursor.move_to(0); - } - } - keyboard::KeyCode::End => { - if modifiers.shift { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), - ); - } else { - self.state.cursor.move_to(self.value.len()); + keyboard::KeyCode::Home => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + 0, + ); + } else { + self.state.cursor.move_to(0); + } } - } - 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 mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, + keyboard::KeyCode::End => { + if modifiers.shift { + self.state.cursor.select_range( + self.state.cursor.start(&self.value), + self.value.len(), ); + } else { + self.state.cursor.move_to(self.value.len()); + } + } + keyboard::KeyCode::V => { + if self.state.keyboard_modifiers.is_command_pressed() { + 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) + } + }; - editor.paste(content.clone()); + let mut editor = Editor::new( + &mut self.value, + &mut self.state.cursor, + ); - let message = (self.on_change)(editor.contents()); - messages.push(message); + editor.paste(content.clone()); - self.state.is_pasting = Some(content); + let message = + (self.on_change)(editor.contents()); + messages.push(message); + + self.state.is_pasting = Some(content); + } + } else { + self.state.is_pasting = None; } - } else { - self.state.is_pasting = None; } - } - keyboard::KeyCode::A => { - if platform::is_copy_paste_modifier_pressed(modifiers) { - self.state.cursor.select_all(&self.value); + keyboard::KeyCode::A => { + if self.state.keyboard_modifiers.is_command_pressed() { + self.state.cursor.select_all(&self.value); + } + } + keyboard::KeyCode::Escape => { + self.state.is_focused = false; + self.state.is_dragging = false; + self.state.is_pasting = None; + + self.state.keyboard_modifiers = + keyboard::Modifiers::default(); } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. - }) => match key_code { - keyboard::KeyCode::V => { - self.state.is_pasting = None; + }) if self.state.is_focused => { + match key_code { + keyboard::KeyCode::V => { + self.state.is_pasting = None; + } + _ => {} } - _ => {} - }, + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) + if self.state.is_focused => + { + self.state.keyboard_modifiers = modifiers; + } _ => {} } + + event::Status::Ignored } fn draw( @@ -481,35 +583,9 @@ where _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, + _viewport: &Rectangle, ) -> Renderer::Output { - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - if self.is_secure { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value.secure(), - &self.state, - &self.style, - ) - } else { - renderer.draw( - bounds, - text_bounds, - cursor_position, - self.font, - self.size.unwrap_or(renderer.default_size()), - &self.placeholder, - &self.value, - &self.state, - &self.style, - ) - } + self.draw(renderer, layout, cursor_position, None) } fn hash_layout(&self, state: &mut Hasher) { @@ -529,34 +605,23 @@ where /// Your [renderer] will need to implement this trait before being /// able to use a [`TextInput`] in your user interface. /// -/// [`TextInput`]: struct.TextInput.html -/// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +/// [renderer]: crate::renderer +pub trait Renderer: text::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; + fn measure_value(&self, value: &str, size: u16, font: Self::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, - font: Font, + font: Self::Font, size: u16, value: &Value, state: &State, @@ -571,16 +636,12 @@ pub trait Renderer: crate::Renderer + Sized { /// - the placeholder to show when the value is empty /// - the current [`Value`] /// - the current [`State`] - /// - /// [`TextInput`]: struct.TextInput.html - /// [`Value`]: struct.Value.html - /// [`State`]: struct.State.html fn draw( &mut self, bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, - font: Font, + font: Self::Font, size: u16, placeholder: &str, value: &Value, @@ -590,12 +651,10 @@ pub trait Renderer: crate::Renderer + Sized { /// Computes the position of the text cursor at the given X coordinate of /// a [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html fn find_cursor_position( &self, text_bounds: Rectangle, - font: Font, + font: Self::Font, size: Option<u16>, value: &Value, state: &State, @@ -620,8 +679,8 @@ pub trait Renderer: crate::Renderer + Sized { impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + self::Renderer, Message: 'a + Clone, + Renderer: 'a + self::Renderer, { fn from( text_input: TextInput<'a, Message, Renderer>, @@ -631,8 +690,6 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Default, Clone)] pub struct State { is_focused: bool, @@ -640,20 +697,17 @@ pub struct State { is_pasting: Option<Value>, last_click: Option<mouse::Click>, cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, // TODO: Add stateful horizontal scrolling offset } impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { Self::default() } /// Creates a new [`State`], representing a focused [`TextInput`]. - /// - /// [`State`]: struct.State.html pub fn focused() -> Self { Self { is_focused: true, @@ -661,30 +715,51 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + keyboard_modifiers: keyboard::Modifiers::default(), } } /// Returns whether the [`TextInput`] is currently focused or not. - /// - /// [`TextInput`]: struct.TextInput.html pub fn is_focused(&self) -> bool { self.is_focused } /// Returns the [`Cursor`] of the [`TextInput`]. - /// - /// [`Cursor`]: struct.Cursor.html - /// [`TextInput`]: struct.TextInput.html pub fn cursor(&self) -> Cursor { self.cursor } + + /// Focuses the [`TextInput`]. + pub fn focus(&mut self) { + self.is_focused = true; + } + + /// Unfocuses the [`TextInput`]. + pub fn unfocus(&mut self) { + self.is_focused = false; + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. + pub fn move_cursor_to_front(&mut self) { + self.cursor.move_to(0); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. + pub fn move_cursor_to_end(&mut self) { + self.cursor.move_to(usize::MAX); + } + + /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. + pub fn move_cursor_to(&mut self, position: usize) { + self.cursor.move_to(position); + } } // TODO: Reduce allocations fn find_cursor_position<Renderer: self::Renderer>( renderer: &Renderer, value: &Value, - font: Font, + font: Renderer::Font, size: u16, target: f32, start: usize, @@ -739,23 +814,11 @@ fn find_cursor_position<Renderer: self::Renderer>( mod platform { use crate::keyboard; - pub fn is_jump_modifier_pressed( - modifiers: keyboard::ModifiersState, - ) -> bool { + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> 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/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index 16e7a01b..e630e293 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -8,8 +8,6 @@ pub struct Cursor { } /// The state of a [`Cursor`]. -/// -/// [`Cursor`]: struct.Cursor.html #[derive(Debug, Copy, Clone)] pub enum State { /// Cursor without a selection @@ -34,9 +32,6 @@ impl Default for Cursor { impl Cursor { /// Returns the [`State`] of the [`Cursor`]. - /// - /// [`State`]: struct.State.html - /// [`Cursor`]: struct.Cursor.html pub fn state(&self, value: &Value) -> State { match self.state { State::Index(index) => State::Index(index.min(value.len())), @@ -166,8 +161,8 @@ impl Cursor { end.min(value.len()) } - pub(crate) fn selection(&self) -> Option<(usize, usize)> { - match self.state { + pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> { + match self.state(value) { State::Selection { start, end } => { Some((start.min(end), start.max(end))) } diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index c9b9795d..20e42567 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -15,7 +15,7 @@ impl<'a> Editor<'a> { } pub fn insert(&mut self, character: char) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((left, right)) => { self.cursor.move_left(self.value); self.value.remove_many(left, right); @@ -30,7 +30,7 @@ impl<'a> Editor<'a> { pub fn paste(&mut self, content: Value) { let length = content.len(); - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((left, right)) => { self.cursor.move_left(self.value); self.value.remove_many(left, right); @@ -44,7 +44,7 @@ impl<'a> Editor<'a> { } pub fn backspace(&mut self) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some((start, end)) => { self.cursor.move_left(self.value); self.value.remove_many(start, end); @@ -61,7 +61,7 @@ impl<'a> Editor<'a> { } pub fn delete(&mut self) { - match self.cursor.selection() { + match self.cursor.selection(self.value) { Some(_) => { self.backspace(); } diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 1e9ba45b..86be2790 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -2,7 +2,7 @@ use unicode_segmentation::UnicodeSegmentation; /// The value of a [`TextInput`]. /// -/// [`TextInput`]: struct.TextInput.html +/// [`TextInput`]: crate::widget::TextInput // TODO: Reduce allocations, cache results (?) #[derive(Debug, Clone)] pub struct Value { @@ -11,8 +11,6 @@ pub struct Value { impl Value { /// Creates a new [`Value`] from a string slice. - /// - /// [`Value`]: struct.Value.html pub fn new(string: &str) -> Self { let graphemes = UnicodeSegmentation::graphemes(string, true) .map(String::from) @@ -21,17 +19,20 @@ impl Value { Self { graphemes } } - /// Returns the total amount of graphemes in the [`Value`]. + /// Returns whether the [`Value`] is empty or not. /// - /// [`Value`]: struct.Value.html + /// A [`Value`] is empty when it contains no graphemes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the total amount of graphemes in the [`Value`]. pub fn len(&self) -> usize { 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(); @@ -54,8 +55,6 @@ impl Value { /// 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(); @@ -76,8 +75,6 @@ impl Value { /// Returns a new [`Value`] containing the graphemes until the given /// `index`. - /// - /// [`Value`]: struct.Value.html pub fn until(&self, index: usize) -> Self { let graphemes = self.graphemes[..index.min(self.len())].to_vec(); @@ -85,8 +82,6 @@ impl Value { } /// Converts the [`Value`] into a `String`. - /// - /// [`Value`]: struct.Value.html pub fn to_string(&self) -> String { self.graphemes.concat() } @@ -109,8 +104,6 @@ impl Value { } /// Removes the grapheme at the given `index`. - /// - /// [`Value`]: struct.Value.html pub fn remove(&mut self, index: usize) { let _ = self.graphemes.remove(index); } @@ -122,8 +115,6 @@ impl Value { /// 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("•")) diff --git a/native/src/window.rs b/native/src/window.rs index 4dcae62f..220bb3be 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,6 +1,4 @@ //! Build window-based GUI applications. -mod backend; mod event; -pub use backend::Backend; pub use event::Event; diff --git a/src/application.rs b/src/application.rs index 689332f1..3b690a7c 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,5 @@ -use crate::{window, Command, Element, Executor, Settings, Subscription}; +use crate::window; +use crate::{Color, Command, Element, Executor, Settings, Subscription}; /// An interactive cross-platform application. /// @@ -10,15 +11,13 @@ use crate::{window, Command, Element, Executor, Settings, Subscription}; /// document. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`](struct.Command.html) in some of its methods. If -/// you do not intend to perform any background work in your program, the -/// [`Sandbox`](trait.Sandbox.html) trait offers a simplified interface. +/// [`Command`] in some of its methods. If you do not intend to perform any +/// background work in your program, the [`Sandbox`] trait offers a simplified +/// interface. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. /// -/// [`Application`]: trait.Application.html -/// /// # Examples /// [The repository has a bunch of examples] that use the [`Application`] trait: /// @@ -28,6 +27,8 @@ use crate::{window, Command, Element, Executor, Settings, Subscription}; /// a dummy file of 100 MB and tracks the download progress. /// - [`events`], a log of native events displayed using a conditional /// [`Subscription`]. +/// - [`game_of_life`], an interactive version of the [Game of Life], invented +/// by [John Horton Conway]. /// - [`pokedex`], an application that displays a random Pokédex entry (sprite /// included!) by using the [PokéAPI]. /// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget @@ -36,17 +37,18 @@ use crate::{window, Command, Element, Executor, Settings, Subscription}; /// to listen to time. /// - [`todos`], a todos tracker inspired by [TodoMVC]. /// -/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/0.1/examples/clock -/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.1/examples/download_progress -/// [`events`]: https://github.com/hecrj/iced/tree/0.1/examples/events -/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.1/examples/pokedex -/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.1/examples/solar_system -/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.1/examples/stopwatch -/// [`todos`]: https://github.com/hecrj/iced/tree/0.1/examples/todos -/// [`Canvas`]: widget/canvas/struct.Canvas.html +/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.2/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/0.2/examples/clock +/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.2/examples/download_progress +/// [`events`]: https://github.com/hecrj/iced/tree/0.2/examples/events +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/0.2/examples/game_of_life +/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.2/examples/pokedex +/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.2/examples/solar_system +/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.2/examples/stopwatch +/// [`todos`]: https://github.com/hecrj/iced/tree/0.2/examples/todos +/// [`Sandbox`]: crate::Sandbox +/// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ -/// [`Subscription`]: type.Subscription.html /// [TodoMVC]: http://todomvc.com/ /// /// ## A simple "Hello, world!" @@ -57,14 +59,14 @@ use crate::{window, Command, Element, Executor, Settings, Subscription}; /// ```no_run /// use iced::{executor, Application, Command, Element, Settings, Text}; /// -/// pub fn main() { +/// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) /// } /// /// struct Hello; /// /// impl Application for Hello { -/// type Executor = executor::Null; +/// type Executor = executor::Default; /// type Message = (); /// type Flags = (); /// @@ -90,18 +92,14 @@ pub trait Application: Sized { /// /// The [default executor] can be a good starting point! /// - /// [`Executor`]: trait.Executor.html - /// [default executor]: executor/struct.Default.html + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - /// - /// [`Application`]: trait.Application.html type Message: std::fmt::Debug + Send; /// The data needed to initialize your [`Application`]. - /// - /// [`Application`]: trait.Application.html type Flags; /// Initializes the [`Application`] with the flags provided to @@ -109,22 +107,17 @@ pub trait Application: Sized { /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`](struct.Command.html) if you - /// need to perform some async action in the background on startup. This is - /// useful if you want to load state from a file, perform an initial HTTP - /// request, etc. + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. /// - /// [`Application`]: trait.Application.html - /// [`run`]: #method.run.html - /// [`Settings`]: struct.Settings.html + /// [`run`]: Self::run fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); /// Returns the current title of the [`Application`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - /// - /// [`Application`]: trait.Application.html fn title(&self) -> String; /// Handles a __message__ and updates the state of the [`Application`]. @@ -134,9 +127,6 @@ pub trait Application: Sized { /// this method. /// /// Any [`Command`] returned will be executed immediately in the background. - /// - /// [`Application`]: trait.Application.html - /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command<Self::Message>; /// Returns the event [`Subscription`] for the current state of the @@ -147,8 +137,6 @@ pub trait Application: Sized { /// [`update`](#tymethod.update). /// /// By default, this method returns an empty [`Subscription`]. - /// - /// [`Subscription`]: struct.Subscription.html fn subscription(&self) -> Subscription<Self::Message> { Subscription::none() } @@ -156,8 +144,6 @@ pub trait Application: Sized { /// 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>; /// Returns the current [`Application`] mode. @@ -168,59 +154,97 @@ pub trait Application: Sized { /// 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 } + /// Returns the background color of the [`Application`]. + /// + /// By default, it returns [`Color::WHITE`]. + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread - /// and __will NOT return__. + /// and __will NOT return__ unless there is an [`Error`] during startup. /// /// It should probably be that last thing you call in your `main` function. /// - /// [`Application`]: trait.Application.html - fn run(settings: Settings<Self::Flags>) + /// [`Error`]: crate::Error + fn run(settings: Settings<Self::Flags>) -> crate::Result where Self: 'static, { #[cfg(not(target_arch = "wasm32"))] { - let wgpu_settings = iced_wgpu::Settings { + let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, + default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { - Some(iced_wgpu::settings::Antialiasing::MSAAx4) + Some(crate::renderer::settings::Antialiasing::MSAAx4) } else { None }, - ..iced_wgpu::Settings::default() + ..crate::renderer::Settings::default() }; - <Instance<Self> as iced_winit::Application>::run( - settings.into(), - wgpu_settings, - ); + Ok(crate::runtime::application::run::< + Instance<Self>, + Self::Executor, + crate::renderer::window::Compositor, + >(settings.into(), renderer_settings)?) } #[cfg(target_arch = "wasm32")] - <Instance<Self> as iced_web::Application>::run(settings.flags); + { + <Instance<Self> as iced_web::Application>::run(settings.flags); + + Ok(()) + } } } struct Instance<A: Application>(A); #[cfg(not(target_arch = "wasm32"))] -impl<A> iced_winit::Application for Instance<A> +impl<A> iced_winit::Program for Instance<A> where A: Application, { - type Backend = iced_wgpu::window::Backend; - type Executor = A::Executor; - type Flags = A::Flags; + type Renderer = crate::renderer::Renderer; type Message = A::Message; + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) + } + + fn view(&mut self) -> Element<'_, Self::Message> { + self.0.view() + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl<A> crate::runtime::Application for Instance<A> +where + A: Application, +{ + type Flags = A::Flags; + fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { let (app, command) = A::new(flags); @@ -238,16 +262,16 @@ where } } - 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() + fn background_color(&self) -> Color { + self.0.background_color() + } + + fn scale_factor(&self) -> f64 { + self.0.scale_factor() } } diff --git a/src/element.rs b/src/element.rs index e5356fb6..6f47c701 100644 --- a/src/element.rs +++ b/src/element.rs @@ -3,7 +3,7 @@ /// 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>; + crate::runtime::Element<'a, Message, crate::renderer::Renderer>; #[cfg(target_arch = "wasm32")] pub use iced_web::Element; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..31b87d17 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,34 @@ +use iced_futures::futures; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The futures executor could not be created. + #[error("the futures executor could not be created")] + ExecutorCreationFailed(futures::io::Error), + + /// The application window could not be created. + #[error("the application window could not be created")] + WindowCreationFailed(Box<dyn std::error::Error>), + + /// A suitable graphics adapter or device could not be found. + #[error("a suitable graphics adapter or device could not be found")] + GraphicsAdapterNotFound, +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<iced_winit::Error> for Error { + fn from(error: iced_winit::Error) -> Error { + match error { + iced_winit::Error::ExecutorCreationFailed(error) => { + Error::ExecutorCreationFailed(error) + } + iced_winit::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(Box::new(error)) + } + iced_winit::Error::GraphicsAdapterNotFound => { + Error::GraphicsAdapterNotFound + } + } + } +} diff --git a/src/executor.rs b/src/executor.rs index 59d59a5a..0333bc1d 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,5 +1,5 @@ //! Choose your preferred executor to power your application. -pub use crate::runtime::{executor::Null, Executor}; +pub use crate::runtime::Executor; pub use platform::Default; @@ -7,13 +7,23 @@ pub use platform::Default; mod platform { use iced_futures::{executor, futures}; - #[cfg(feature = "tokio")] + #[cfg(feature = "tokio_old")] + type Executor = executor::TokioOld; + + #[cfg(all(not(feature = "tokio_old"), feature = "tokio"))] type Executor = executor::Tokio; - #[cfg(all(not(feature = "tokio"), feature = "async-std"))] + #[cfg(all( + not(any(feature = "tokio_old", feature = "tokio")), + feature = "async-std" + ))] type Executor = executor::AsyncStd; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] + #[cfg(not(any( + feature = "tokio_old", + feature = "tokio", + feature = "async-std" + )))] type Executor = executor::ThreadPool; /// A default cross-platform executor. @@ -40,7 +50,7 @@ mod platform { } fn enter<R>(&self, f: impl FnOnce() -> R) -> R { - self.0.enter(f) + super::Executor::enter(&self.0, f) } } } diff --git a/src/keyboard.rs b/src/keyboard.rs index 0b3e894d..2134a66b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,2 +1,2 @@ //! Listen and react to keyboard events. -pub use crate::runtime::keyboard::{Event, KeyCode, ModifiersState}; +pub use crate::runtime::keyboard::{Event, KeyCode, Modifiers}; @@ -30,7 +30,7 @@ //! [windowing shell]: https://github.com/hecrj/iced/tree/master/winit //! [`dodrio`]: https://github.com/fitzgen/dodrio //! [web runtime]: https://github.com/hecrj/iced/tree/master/web -//! [examples]: https://github.com/hecrj/iced/tree/0.1/examples +//! [examples]: https://github.com/hecrj/iced/tree/0.2/examples //! [repository]: https://github.com/hecrj/iced //! //! # Overview @@ -171,8 +171,6 @@ //! //! [Elm]: https://elm-lang.org/ //! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [`Application`]: trait.Application.html -//! [`Sandbox`]: trait.Sandbox.html #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] @@ -181,6 +179,8 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod application; mod element; +mod error; +mod result; mod sandbox; pub mod executor; @@ -191,27 +191,53 @@ pub mod widget; pub mod window; #[cfg(all( - any(feature = "tokio", feature = "async-std"), + any(feature = "tokio", feature = "tokio_old", feature = "async-std"), not(target_arch = "wasm32") ))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "tokio", + feature = "tokio_old", + feature = "async-std" + ))) +)] pub mod time; +#[cfg(all( + not(target_arch = "wasm32"), + not(feature = "glow"), + feature = "wgpu" +))] +use iced_winit as runtime; + +#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))] +use iced_glutin as runtime; + +#[cfg(all( + not(target_arch = "wasm32"), + not(feature = "glow"), + feature = "wgpu" +))] +use iced_wgpu as renderer; + +#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))] +use iced_glow as renderer; + +#[cfg(target_arch = "wasm32")] +use iced_web as runtime; + #[doc(no_inline)] pub use widget::*; pub use application::Application; pub use element::Element; +pub use error::Error; pub use executor::Executor; +pub use result::Result; pub use sandbox::Sandbox; pub use settings::Settings; -#[cfg(not(target_arch = "wasm32"))] -use iced_winit as runtime; - -#[cfg(target_arch = "wasm32")] -use iced_web as runtime; - pub use runtime::{ futures, Align, Background, Color, Command, Font, HorizontalAlignment, Length, Point, Rectangle, Size, Subscription, Vector, VerticalAlignment, diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 00000000..ef565bd6 --- /dev/null +++ b/src/result.rs @@ -0,0 +1,6 @@ +use crate::Error; + +/// The result of running an [`Application`]. +/// +/// [`Application`]: crate::Application +pub type Result = std::result::Result<(), Error>; diff --git a/src/sandbox.rs b/src/sandbox.rs index c6fa45d0..dbaa02f1 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,4 +1,6 @@ -use crate::{executor, Application, Command, Element, Settings, Subscription}; +use crate::{ + Application, Color, Command, Element, Error, Settings, Subscription, +}; /// A sandboxed [`Application`]. /// @@ -12,11 +14,6 @@ use crate::{executor, Application, Command, Element, Settings, Subscription}; /// Therefore, it is recommended to always start by implementing this trait and /// upgrade only once necessary. /// -/// [`Application`]: trait.Application.html -/// [`Sandbox`]: trait.Sandbox.html -/// [`Command`]: struct.Command.html -/// [`Command::none`]: struct.Command.html#method.none -/// /// # Examples /// [The repository has a bunch of examples] that use the [`Sandbox`] trait: /// @@ -38,20 +35,20 @@ use crate::{executor, Application, Command, Element, Settings, Subscription}; /// - [`tour`], a simple UI tour that can run both on native platforms and the /// web! /// -/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.1/examples/bezier_tool -/// [`counter`]: https://github.com/hecrj/iced/tree/0.1/examples/counter -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.1/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.1/examples/geometry -/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.1/examples/progress_bar -/// [`styling`]: https://github.com/hecrj/iced/tree/0.1/examples/styling -/// [`svg`]: https://github.com/hecrj/iced/tree/0.1/examples/svg -/// [`tour`]: https://github.com/hecrj/iced/tree/0.1/examples/tour +/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.2/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool +/// [`counter`]: https://github.com/hecrj/iced/tree/0.2/examples/counter +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry +/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid +/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.2/examples/progress_bar +/// [`styling`]: https://github.com/hecrj/iced/tree/0.2/examples/styling +/// [`svg`]: https://github.com/hecrj/iced/tree/0.2/examples/svg +/// [`tour`]: https://github.com/hecrj/iced/tree/0.2/examples/tour /// [`lyon`]: https://github.com/nical/lyon /// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.1/wgpu -/// [`Svg` widget]: widget/svg/struct.Svg.html +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu +/// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// /// ## A simple "Hello, world!" @@ -62,7 +59,7 @@ use crate::{executor, Application, Command, Element, Settings, Subscription}; /// ```no_run /// use iced::{Element, Sandbox, Settings, Text}; /// -/// pub fn main() { +/// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) /// } /// @@ -90,49 +87,57 @@ use crate::{executor, Application, Command, Element, Settings, Subscription}; /// ``` pub trait Sandbox { /// The type of __messages__ your [`Sandbox`] will produce. - /// - /// [`Sandbox`]: trait.Sandbox.html type Message: std::fmt::Debug + Send; /// Initializes the [`Sandbox`]. /// /// Here is where you should return the initial state of your app. - /// - /// [`Sandbox`]: trait.Sandbox.html fn new() -> Self; /// Returns the current title of the [`Sandbox`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - /// - /// [`Sandbox`]: trait.Sandbox.html fn title(&self) -> String; /// Handles a __message__ and updates the state of the [`Sandbox`]. /// /// This is where you define your __update logic__. All the __messages__, /// produced by user interactions, will be handled by this method. - /// - /// [`Sandbox`]: trait.Sandbox.html fn update(&mut self, message: Self::Message); /// Returns the widgets to display in the [`Sandbox`]. /// /// These widgets can produce __messages__ based on user interaction. - /// - /// [`Sandbox`]: trait.Sandbox.html fn view(&mut self) -> Element<'_, Self::Message>; + /// Returns the background color of the [`Sandbox`]. + /// + /// By default, it returns [`Color::WHITE`]. + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Sandbox`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } + /// Runs the [`Sandbox`]. /// /// On native platforms, this method will take control of the current thread /// and __will NOT return__. /// /// It should probably be that last thing you call in your `main` function. - /// - /// [`Sandbox`]: trait.Sandbox.html - fn run(settings: Settings<()>) + fn run(settings: Settings<()>) -> Result<(), Error> where Self: 'static + Sized, { @@ -144,7 +149,7 @@ impl<T> Application for T where T: Sandbox, { - type Executor = executor::Null; + type Executor = crate::runtime::executor::Null; type Flags = (); type Message = T::Message; @@ -169,4 +174,12 @@ where fn view(&mut self) -> Element<'_, T::Message> { T::view(self) } + + fn background_color(&self) -> Color { + T::background_color(self) + } + + fn scale_factor(&self) -> f64 { + T::scale_factor(self) + } } diff --git a/src/settings.rs b/src/settings.rs index 01ad0ee0..c82a1354 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,18 +2,16 @@ use crate::window; /// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone)] pub struct Settings<Flags> { /// The window settings. /// /// They will be ignored on the Web. - /// - /// [`Window`]: struct.Window.html pub window: window::Settings, /// The data needed to initialize an [`Application`]. /// - /// [`Application`]: ../trait.Application.html + /// [`Application`]: crate::Application pub flags: Flags, /// The bytes of the font that will be used by default. @@ -22,6 +20,11 @@ pub struct Settings<Flags> { // TODO: Add `name` for web compatibility pub default_font: Option<&'static [u8]>, + /// The text size that will be used by default. + /// + /// The default value is 20. + pub default_text_size: u16, + /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// @@ -30,21 +33,37 @@ pub struct Settings<Flags> { /// /// By default, it is disabled. /// - /// [`Canvas`]: ../widget/canvas/struct.Canvas.html + /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, } impl<Flags> Settings<Flags> { - /// Initialize application settings using the given data. + /// Initialize [`Application`] settings using the given data. /// - /// [`Application`]: ../trait.Application.html + /// [`Application`]: crate::Application pub fn with_flags(flags: Flags) -> Self { + let default_settings = Settings::<()>::default(); + Self { flags, - // not using ..Default::default() struct update syntax since it is more permissive to - // allow initializing with flags without trait bound on Default + antialiasing: default_settings.antialiasing, + default_font: default_settings.default_font, + default_text_size: default_settings.default_text_size, + window: default_settings.window, + } + } +} + +impl<Flags> Default for Settings<Flags> +where + Flags: Default, +{ + fn default() -> Self { + Self { + flags: Default::default(), antialiasing: Default::default(), default_font: Default::default(), + default_text_size: 20, window: Default::default(), } } @@ -54,12 +73,7 @@ impl<Flags> Settings<Flags> { impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { - window: iced_winit::settings::Window { - size: settings.window.size, - resizable: settings.window.resizable, - decorations: settings.window.decorations, - platform_specific: Default::default(), - }, + window: settings.window.into(), flags: settings.flags, } } diff --git a/src/time.rs b/src/time.rs index cd442461..b8432895 100644 --- a/src/time.rs +++ b/src/time.rs @@ -5,8 +5,6 @@ use crate::Subscription; /// /// The first message is produced after a `duration`, and then continues to /// produce more messages every `duration` after that. -/// -/// [`Subscription`]: ../subscription/struct.Subscription.html pub fn every( duration: std::time::Duration, ) -> Subscription<std::time::Instant> { diff --git a/src/widget.rs b/src/widget.rs index 932a8cf6..edd35d2d 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -13,59 +13,55 @@ //! //! 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::{ - button, checkbox, container, pane_grid, progress_bar, radio, - scrollable, slider, text_input, Text, + pub use crate::renderer::widget::{ + button, checkbox, container, pane_grid, pick_list, progress_bar, radio, + rule, scrollable, slider, text_input, Column, Row, Space, Text, }; - #[cfg(feature = "canvas")] - #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] - pub use iced_wgpu::widget::canvas; + #[cfg(any(feature = "canvas", feature = "glow_canvas"))] + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "canvas", feature = "glow_canvas"))) + )] + pub use crate::renderer::widget::canvas; + + #[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "qr_code", feature = "glow_qr_code"))) + )] + pub use crate::renderer::widget::qr_code; #[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub mod image { //! Display images in your user interface. - pub use iced_winit::image::{Handle, Image, Viewer}; - - pub use iced_winit::image::viewer; + pub use crate::runtime::image::viewer; + pub use crate::runtime::image::{Handle, Image, Viewer}; } #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub mod svg { //! Display vector graphics in your user interface. - pub use iced_winit::svg::{Handle, Svg}; + pub use crate::runtime::svg::{Handle, Svg}; } - pub use iced_winit::Space; - #[doc(no_inline)] pub use { button::Button, checkbox::Checkbox, container::Container, image::Image, - pane_grid::PaneGrid, progress_bar::ProgressBar, radio::Radio, - scrollable::Scrollable, slider::Slider, svg::Svg, - text_input::TextInput, + pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, + radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, + svg::Svg, text_input::TextInput, }; - #[cfg(feature = "canvas")] + #[cfg(any(feature = "canvas", feature = "glow_canvas"))] #[doc(no_inline)] pub use canvas::Canvas; - /// 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(any(feature = "qr_code", feature = "glow_qr_code"))] + #[doc(no_inline)] + pub use qr_code::QRCode; } #[cfg(target_arch = "wasm32")] diff --git a/src/window.rs b/src/window.rs index 54ea2a02..a2883b62 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,5 +2,8 @@ mod mode; mod settings; +pub mod icon; + +pub use icon::Icon; pub use mode::Mode; pub use settings::Settings; diff --git a/src/window/icon.rs b/src/window/icon.rs new file mode 100644 index 00000000..0d27b00e --- /dev/null +++ b/src/window/icon.rs @@ -0,0 +1,132 @@ +//! Attach an icon to the window of your application. +use std::fmt; +use std::io; + +/// The icon of a window. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Clone)] +pub struct Icon(iced_winit::winit::window::Icon); + +/// The icon of a window. +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone)] +pub struct Icon; + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + #[cfg(not(target_arch = "wasm32"))] + pub fn from_rgba( + rgba: Vec<u8>, + width: u32, + height: u32, + ) -> Result<Self, Error> { + let raw = + iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?; + + Ok(Icon(raw)) + } + + /// Creates an icon from 32bpp RGBA data. + #[cfg(target_arch = "wasm32")] + pub fn from_rgba( + _rgba: Vec<u8>, + _width: u32, + _height: u32, + ) -> Result<Self, Error> { + Ok(Icon) + } +} + +/// An error produced when using `Icon::from_rgba` with invalid arguments. +#[derive(Debug)] +pub enum Error { + /// The provided RGBA data isn't divisble by 4. + /// + /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. + InvalidData { + /// The length of the provided RGBA data. + byte_count: usize, + }, + + /// The number of RGBA pixels does not match the provided dimensions. + DimensionsMismatch { + /// The provided width. + width: u32, + /// The provided height. + height: u32, + /// The amount of pixels of the provided RGBA data. + pixel_count: usize, + }, + + /// The underlying OS failed to create the icon. + OsError(io::Error), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<iced_winit::winit::window::BadIcon> for Error { + fn from(error: iced_winit::winit::window::BadIcon) -> Self { + use iced_winit::winit::window::BadIcon; + + match error { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => { + Error::InvalidData { byte_count } + } + BadIcon::DimensionsVsPixelCount { + width, + height, + pixel_count, + .. + } => Error::DimensionsMismatch { + width, + height, + pixel_count, + }, + BadIcon::OsError(os_error) => Error::OsError(os_error), + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<Icon> for iced_winit::winit::window::Icon { + fn from(icon: Icon) -> Self { + icon.0 + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidData { byte_count } => { + write!(f, + "The provided RGBA data (with length {:?}) isn't divisble by \ + 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \ + pixels.", + byte_count, + ) + } + Error::DimensionsMismatch { + width, + height, + pixel_count, + } => { + write!(f, + "The number of RGBA pixels ({:?}) does not match the provided \ + dimensions ({:?}x{:?}).", + pixel_count, width, height, + ) + } + Error::OsError(e) => write!( + f, + "The underlying OS failed to create the window \ + icon: {:?}", + e + ), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self) + } +} diff --git a/src/window/settings.rs b/src/window/settings.rs index a31d2af2..6b5d2985 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,22 +1,61 @@ +use crate::window::Icon; + /// The window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct Settings { - /// The size of the window. + /// The initial size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(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, + + /// Whether the window should be transparent. + pub transparent: bool, + + /// Whether the window will always be on top of other windows. + pub always_on_top: bool, + + /// The icon of the window. + pub icon: Option<Icon>, } impl Default for Settings { fn default() -> Settings { Settings { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, + transparent: false, + always_on_top: false, + icon: None, + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<Settings> for iced_winit::settings::Window { + fn from(settings: Settings) -> Self { + Self { + size: settings.size, + min_size: settings.min_size, + max_size: settings.max_size, + resizable: settings.resizable, + decorations: settings.decorations, + transparent: settings.transparent, + always_on_top: settings.always_on_top, + icon: settings.icon.map(Icon::into), + platform_specific: Default::default(), } } } diff --git a/style/Cargo.toml b/style/Cargo.toml index abc64c0f..ac16f8ee 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_style" -version = "0.1.0" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "The default set of styles of Iced" @@ -11,4 +11,4 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [dependencies] -iced_core = { version = "0.2", path = "../core" } +iced_core = { version = "0.3", path = "../core" } diff --git a/style/src/button.rs b/style/src/button.rs index 1e3844f9..43d27216 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -6,8 +6,8 @@ use iced_core::{Background, Color, Vector}; pub struct Style { pub shadow_offset: Vector, pub background: Option<Background>, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, pub text_color: Color, } @@ -17,8 +17,8 @@ impl std::default::Default for Style { Self { shadow_offset: Vector::default(), background: None, - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, text_color: Color::BLACK, } @@ -72,8 +72,8 @@ impl StyleSheet for Default { 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_radius: 2.0, + border_width: 1.0, border_color: [0.7, 0.7, 0.7].into(), text_color: Color::BLACK, } diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs index 3c645f15..1c5f2460 100644 --- a/style/src/checkbox.rs +++ b/style/src/checkbox.rs @@ -6,8 +6,8 @@ use iced_core::{Background, Color}; pub struct Style { pub background: Background, pub checkmark_color: Color, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, } @@ -25,8 +25,8 @@ impl StyleSheet for Default { 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_radius: 5.0, + border_width: 1.0, border_color: Color::from_rgb(0.6, 0.6, 0.6), } } diff --git a/style/src/container.rs b/style/src/container.rs index d2247342..1ce6a7ca 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -6,8 +6,8 @@ use iced_core::{Background, Color}; pub struct Style { pub text_color: Option<Color>, pub background: Option<Background>, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, } @@ -16,8 +16,8 @@ impl std::default::Default for Style { Self { text_color: None, background: None, - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, } } @@ -36,8 +36,8 @@ impl StyleSheet for Default { Style { text_color: None, background: None, - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, } } diff --git a/style/src/lib.rs b/style/src/lib.rs index 2c5977b5..7e0a9f49 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -2,11 +2,18 @@ //! //! It contains a set of styles and stylesheets for most of the built-in //! widgets. +//! +//!  +pub use iced_core::{Background, Color}; + pub mod button; pub mod checkbox; pub mod container; +pub mod menu; +pub mod pick_list; pub mod progress_bar; pub mod radio; +pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; diff --git a/style/src/menu.rs b/style/src/menu.rs new file mode 100644 index 00000000..90985b8f --- /dev/null +++ b/style/src/menu.rs @@ -0,0 +1,25 @@ +use iced_core::{Background, Color}; + +/// The appearance of a menu. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub text_color: Color, + pub background: Background, + pub border_width: f32, + pub border_color: Color, + pub selected_text_color: Color, + pub selected_background: Background, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + text_color: Color::BLACK, + background: Background::Color([0.87, 0.87, 0.87].into()), + border_width: 1.0, + border_color: [0.7, 0.7, 0.7].into(), + selected_text_color: Color::WHITE, + selected_background: Background::Color([0.4, 0.4, 1.0].into()), + } + } +} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs new file mode 100644 index 00000000..a757ba98 --- /dev/null +++ b/style/src/pick_list.rs @@ -0,0 +1,70 @@ +use crate::menu; +use iced_core::{Background, Color}; + +/// The appearance of a pick list. +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub text_color: Color, + pub background: Background, + pub border_radius: f32, + pub border_width: f32, + pub border_color: Color, + pub icon_size: f32, +} + +impl std::default::Default for Style { + fn default() -> Self { + Self { + text_color: Color::BLACK, + background: Background::Color([0.87, 0.87, 0.87].into()), + border_radius: 0.0, + border_width: 1.0, + border_color: [0.7, 0.7, 0.7].into(), + icon_size: 0.7, + } + } +} + +/// A set of rules that dictate the style of a container. +pub trait StyleSheet { + fn menu(&self) -> menu::Style; + + fn active(&self) -> Style; + + /// Produces the style of a container. + fn hovered(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn menu(&self) -> menu::Style { + menu::Style::default() + } + + fn active(&self) -> Style { + Style::default() + } + + fn hovered(&self) -> Style { + Style { + border_color: Color::BLACK, + ..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/progress_bar.rs b/style/src/progress_bar.rs index 73503fa8..36be63f9 100644 --- a/style/src/progress_bar.rs +++ b/style/src/progress_bar.rs @@ -6,7 +6,7 @@ use iced_core::{Background, Color}; pub struct Style { pub background: Background, pub bar: Background, - pub border_radius: u16, + pub border_radius: f32, } /// A set of rules that dictate the style of a progress bar. @@ -21,7 +21,7 @@ impl StyleSheet for Default { 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, + border_radius: 5.0, } } } diff --git a/style/src/radio.rs b/style/src/radio.rs index 1f0689b9..83310e05 100644 --- a/style/src/radio.rs +++ b/style/src/radio.rs @@ -6,7 +6,7 @@ use iced_core::{Background, Color}; pub struct Style { pub background: Background, pub dot_color: Color, - pub border_width: u16, + pub border_width: f32, pub border_color: Color, } @@ -24,7 +24,7 @@ impl StyleSheet for Default { 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_width: 1.0, border_color: Color::from_rgb(0.6, 0.6, 0.6), } } diff --git a/style/src/rule.rs b/style/src/rule.rs new file mode 100644 index 00000000..5021340b --- /dev/null +++ b/style/src/rule.rs @@ -0,0 +1,114 @@ +//! Display a horizontal or vertical rule for dividing content. + +use iced_core::Color; + +/// The fill mode of a rule. +#[derive(Debug, Clone, Copy)] +pub enum FillMode { + /// Fill the whole length of the container. + Full, + /// Fill a percent of the length of the container. The rule + /// will be centered in that container. + /// + /// The range is `[0.0, 100.0]`. + Percent(f32), + /// Uniform offset from each end, length units. + Padded(u16), + /// Different offset on each end of the rule, length units. + /// First = top or left. + AsymmetricPadding(u16, u16), +} + +impl FillMode { + /// Return the starting offset and length of the rule. + /// + /// * `space` - The space to fill. + /// + /// # Returns + /// + /// * (starting_offset, length) + pub fn fill(&self, space: f32) -> (f32, f32) { + match *self { + FillMode::Full => (0.0, space), + FillMode::Percent(percent) => { + if percent >= 100.0 { + (0.0, space) + } else { + let percent_width = (space * percent / 100.0).round(); + + (((space - percent_width) / 2.0).round(), percent_width) + } + } + FillMode::Padded(padding) => { + if padding == 0 { + (0.0, space) + } else { + let padding = padding as f32; + let mut line_width = space - (padding * 2.0); + if line_width < 0.0 { + line_width = 0.0; + } + + (padding, line_width) + } + } + FillMode::AsymmetricPadding(first_pad, second_pad) => { + let first_pad = first_pad as f32; + let second_pad = second_pad as f32; + let mut line_width = space - first_pad - second_pad; + if line_width < 0.0 { + line_width = 0.0; + } + + (first_pad, line_width) + } + } + } +} + +/// The appearance of a rule. +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// The color of the rule. + pub color: Color, + /// The width (thickness) of the rule line. + pub width: u16, + /// The radius of the line corners. + pub radius: f32, + /// The [`FillMode`] of the rule. + pub fill_mode: FillMode, +} + +/// A set of rules that dictate the style of a rule. +pub trait StyleSheet { + /// Produces the style of a rule. + fn style(&self) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn style(&self) -> Style { + Style { + color: [0.6, 0.6, 0.6, 0.51].into(), + width: 1, + radius: 0.0, + fill_mode: FillMode::Percent(90.0), + } + } +} + +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 index 690c14a2..65da9803 100644 --- a/style/src/scrollable.rs +++ b/style/src/scrollable.rs @@ -5,8 +5,8 @@ use iced_core::{Background, Color}; #[derive(Debug, Clone, Copy)] pub struct Scrollbar { pub background: Option<Background>, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, pub scroller: Scroller, } @@ -15,8 +15,8 @@ pub struct Scrollbar { #[derive(Debug, Clone, Copy)] pub struct Scroller { pub color: Color, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, } @@ -40,13 +40,13 @@ impl StyleSheet for Default { fn active(&self) -> Scrollbar { Scrollbar { background: None, - border_radius: 5, - border_width: 0, + border_radius: 5.0, + border_width: 0.0, border_color: Color::TRANSPARENT, scroller: Scroller { color: [0.0, 0.0, 0.0, 0.7].into(), - border_radius: 5, - border_width: 0, + border_radius: 5.0, + border_width: 0.0, border_color: Color::TRANSPARENT, }, } diff --git a/style/src/slider.rs b/style/src/slider.rs index 776e180c..9148fcbe 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -13,15 +13,15 @@ pub struct Style { pub struct Handle { pub shape: HandleShape, pub color: Color, - pub border_width: u16, + pub border_width: f32, 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 }, + Circle { radius: f32 }, + Rectangle { width: u16, border_radius: f32 }, } /// A set of rules that dictate the style of a slider. @@ -45,11 +45,11 @@ impl StyleSheet for Default { handle: Handle { shape: HandleShape::Rectangle { width: 8, - border_radius: 4, + border_radius: 4.0, }, color: Color::from_rgb(0.95, 0.95, 0.95), border_color: Color::from_rgb(0.6, 0.6, 0.6), - border_width: 1, + border_width: 1.0, }, } } diff --git a/style/src/text_input.rs b/style/src/text_input.rs index 1cb72364..19acea65 100644 --- a/style/src/text_input.rs +++ b/style/src/text_input.rs @@ -5,8 +5,8 @@ use iced_core::{Background, Color}; #[derive(Debug, Clone, Copy)] pub struct Style { pub background: Background, - pub border_radius: u16, - pub border_width: u16, + pub border_radius: f32, + pub border_width: f32, pub border_color: Color, } @@ -14,8 +14,8 @@ impl std::default::Default for Style { fn default() -> Self { Self { background: Background::Color(Color::WHITE), - border_radius: 0, - border_width: 0, + border_radius: 0.0, + border_width: 0.0, border_color: Color::TRANSPARENT, } } @@ -47,8 +47,8 @@ impl StyleSheet for Default { fn active(&self) -> Style { Style { background: Background::Color(Color::WHITE), - border_radius: 5, - border_width: 1, + border_radius: 5.0, + border_width: 1.0, border_color: Color::from_rgb(0.7, 0.7, 0.7), } } diff --git a/web/Cargo.toml b/web/Cargo.toml index 12d3865e..e063a021 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_web" -version = "0.2.1" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A web backend for Iced" @@ -15,21 +15,22 @@ categories = ["web-programming"] maintenance = { status = "actively-developed" } [dependencies] -dodrio = "0.1.0" -wasm-bindgen = "0.2.51" +dodrio = "0.2" +wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" url = "2.0" +num-traits = "0.2" [dependencies.iced_core] -version = "0.2" +version = "0.3" path = "../core" [dependencies.iced_futures] -version = "0.1" +version = "0.2" path = "../futures" [dependencies.iced_style] -version = "0.1" +version = "0.2" path = "../style" [dependencies.web-sys] diff --git a/web/README.md b/web/README.md index 810bd1ec..0e770589 100644 --- a/web/README.md +++ b/web/README.md @@ -8,8 +8,6 @@ The crate is currently a __very experimental__, simple abstraction layer over [`dodrio`]. - - [documentation]: https://docs.rs/iced_web [`iced_core`]: ../core [`dodrio`]: https://github.com/fitzgen/dodrio @@ -18,7 +16,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.2" +iced_web = "0.3" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/web/src/bus.rs b/web/src/bus.rs index c66e9659..5ce8e810 100644 --- a/web/src/bus.rs +++ b/web/src/bus.rs @@ -5,7 +5,7 @@ use std::rc::Rc; /// /// It can be used to route messages back to the [`Application`]. /// -/// [`Application`]: trait.Application.html +/// [`Application`]: crate::Application #[allow(missing_debug_implementations)] pub struct Bus<Message> { publish: Rc<Box<dyn Fn(Message) -> ()>>, @@ -33,15 +33,13 @@ where /// Publishes a new message for the [`Application`]. /// - /// [`Application`]: trait.Application.html + /// [`Application`]: crate::Application pub fn publish(&self, message: Message) { (self.publish)(message) } /// Creates a new [`Bus`] that applies the given function to the messages /// before publishing. - /// - /// [`Bus`]: struct.Bus.html pub fn map<B>(&self, mapper: Rc<Box<dyn Fn(B) -> Message>>) -> Bus<B> where B: 'static, diff --git a/web/src/css.rs b/web/src/css.rs index 6a307770..bdde23f3 100644 --- a/web/src/css.rs +++ b/web/src/css.rs @@ -20,9 +20,7 @@ pub enum Rule { } impl Rule { - /// Returns the class name of the [`Style`]. - /// - /// [`Style`]: enum.Style.html + /// Returns the class name of the [`Rule`]. pub fn class<'a>(&self) -> String { match self { Rule::Column => String::from("c"), @@ -32,9 +30,7 @@ impl Rule { } } - /// Returns the declaration of the [`Style`]. - /// - /// [`Style`]: enum.Style.html + /// Returns the declaration of the [`Rule`]. pub fn declaration<'a>(&self, bump: &'a bumpalo::Bump) -> &'a str { let class = self.class(); @@ -81,22 +77,17 @@ pub struct Css<'a> { } impl<'a> Css<'a> { - /// Creates an empty style [`Sheet`]. - /// - /// [`Sheet`]: struct.Sheet.html + /// Creates an empty [`Css`]. pub fn new() -> Self { Css { rules: BTreeMap::new(), } } - /// Inserts the [`rule`] in the [`Sheet`], if it was not previously + /// Inserts the [`Rule`] in the [`Css`], if it was not previously /// inserted. /// /// It returns the class name of the provided [`Rule`]. - /// - /// [`Sheet`]: struct.Sheet.html - /// [`Rule`]: enum.Rule.html pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String { let class = rule.class(); @@ -107,9 +98,7 @@ impl<'a> Css<'a> { class } - /// Produces the VDOM node of the style [`Sheet`]. - /// - /// [`Sheet`]: struct.Sheet.html + /// Produces the VDOM node of the [`Css`]. pub fn node(self, bump: &'a bumpalo::Bump) -> dodrio::Node<'a> { use dodrio::builder::*; @@ -133,8 +122,6 @@ impl<'a> Css<'a> { } /// Returns the style value for the given [`Length`]. -/// -/// [`Length`]: ../enum.Length.html pub fn length(length: Length) -> String { match length { Length::Shrink => String::from("auto"), @@ -164,15 +151,11 @@ pub fn min_length(units: u32) -> String { } /// Returns the style value for the given [`Color`]. -/// -/// [`Color`]: ../struct.Color.html 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), @@ -180,8 +163,6 @@ pub fn background(background: Background) -> String { } /// Returns the style value for the given [`Align`]. -/// -/// [`Align`]: ../enum.Align.html pub fn align(align: Align) -> &'static str { match align { Align::Start => "flex-start", diff --git a/web/src/element.rs b/web/src/element.rs index 93e73713..6bb90177 100644 --- a/web/src/element.rs +++ b/web/src/element.rs @@ -11,9 +11,7 @@ use std::rc::Rc; /// If you have a [built-in widget], you should be able to use `Into<Element>` /// to turn it into an [`Element`]. /// -/// [built-in widget]: widget/index.html -/// [`Widget`]: widget/trait.Widget.html -/// [`Element`]: struct.Element.html +/// [built-in widget]: mod@crate::widget #[allow(missing_debug_implementations)] pub struct Element<'a, Message> { pub(crate) widget: Box<dyn Widget<Message> + 'a>, @@ -21,9 +19,6 @@ pub struct Element<'a, Message> { impl<'a, Message> Element<'a, Message> { /// Create a new [`Element`] containing the given [`Widget`]. - /// - /// [`Element`]: struct.Element.html - /// [`Widget`]: widget/trait.Widget.html pub fn new(widget: impl Widget<Message> + 'a) -> Self { Self { widget: Box::new(widget), @@ -34,8 +29,6 @@ impl<'a, Message> Element<'a, Message> { /// /// This method is useful when you want to decouple different parts of your /// UI and make them __composable__. - /// - /// [`Element`]: struct.Element.html pub fn map<F, B>(self, f: F) -> Element<'a, B> where Message: 'static, diff --git a/web/src/lib.rs b/web/src/lib.rs index 53b54b7e..58f6591d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1,7 +1,5 @@ //! A web runtime for Iced, targetting the DOM. //! -//!  -//! //! `iced_web` takes [`iced_core`] and builds a WebAssembly runtime on top. It //! achieves this by introducing a `Widget` trait that can be used to produce //! VDOM nodes. @@ -51,7 +49,7 @@ //! //! [`wasm-pack`]: https://github.com/rustwasm/wasm-pack //! [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen -//! [`tour` example]: https://github.com/hecrj/iced/tree/0.1/examples/tour +//! [`tour` example]: https://github.com/hecrj/iced/tree/0.2/examples/tour #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] @@ -96,33 +94,21 @@ pub use executor::Executor; /// by returning a [`Command`](struct.Command.html) in some of its methods. pub trait Application { /// 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; /// The type of __messages__ your [`Application`] will produce. - /// - /// [`Application`]: trait.Application.html type Message: Send; /// The data needed to initialize your [`Application`]. - /// - /// [`Application`]: trait.Application.html type Flags; /// Initializes the [`Application`]. /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`](struct.Command.html) if you - /// need to perform some async action in the background on startup. This is - /// useful if you want to load state from a file, perform an initial HTTP - /// request, etc. - /// - /// [`Application`]: trait.Application.html + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) where Self: Sized; @@ -131,8 +117,6 @@ pub trait Application { /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - /// - /// [`Application`]: trait.Application.html fn title(&self) -> String; /// Handles a __message__ and updates the state of the [`Application`]. @@ -142,16 +126,11 @@ pub trait Application { /// this method. /// /// Any [`Command`] returned will be executed immediately in the background. - /// - /// [`Application`]: trait.Application.html - /// [`Command`]: struct.Command.html fn update(&mut self, message: Self::Message) -> Command<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>; /// Returns the event [`Subscription`] for the current state of the @@ -162,15 +141,11 @@ pub trait Application { /// [`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 fn run(flags: Self::Flags) where Self: 'static + Sized, @@ -238,28 +213,25 @@ struct Instance<A: Application> { bus: Bus<A::Message>, } -impl<A> dodrio::Render for Instance<A> +impl<'a, A> dodrio::Render<'a> for Instance<A> where A: Application, { - fn render<'a, 'bump>( - &'a self, - bump: &'bump bumpalo::Bump, - ) -> dodrio::Node<'bump> - where - 'a: 'bump, - { + fn render( + &self, + context: &mut dodrio::RenderContext<'a>, + ) -> dodrio::Node<'a> { use dodrio::builder::*; let mut ui = self.application.borrow_mut(); let element = ui.view(); let mut css = Css::new(); - let node = element.widget.node(bump, &self.bus, &mut css); + let node = element.widget.node(context.bump, &self.bus, &mut css); - div(bump) + div(context.bump) .attr("style", "width: 100%; height: 100%") - .children(vec![css.node(bump), node]) + .children(vec![css.node(context.bump), node]) .finish() } } diff --git a/web/src/subscription.rs b/web/src/subscription.rs index 6b8415c0..fb54f7e3 100644 --- a/web/src/subscription.rs +++ b/web/src/subscription.rs @@ -12,8 +12,7 @@ use crate::Hasher; /// 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 +/// [`Command`]: crate::Command 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 025cf22f..023f5f13 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -12,8 +12,6 @@ //! ``` //! use iced_web::{button, Button, Widget}; //! ``` -//! -//! [`Widget`]: trait.Widget.html use crate::{Bus, Css}; use dodrio::bumpalo; @@ -56,12 +54,8 @@ pub use space::Space; /// /// If you want to build your own widgets, you will need to implement this /// trait. -/// -/// [`Widget`]: trait.Widget.html pub trait Widget<Message> { /// Produces a VDOM node for the [`Widget`]. - /// - /// [`Widget`]: trait.Widget.html fn node<'b>( &self, bump: &'b bumpalo::Bump, diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs index 3a5afe60..e7cff6a0 100644 --- a/web/src/widget/button.rs +++ b/web/src/widget/button.rs @@ -1,9 +1,6 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -//! -//! [`Button`]: struct.Button.html -//! [`State`]: struct.State.html use crate::{css, Background, Bus, Css, Element, Length, Widget}; pub use iced_style::button::{Style, StyleSheet}; @@ -38,9 +35,6 @@ pub struct Button<'a, Message> { impl<'a, Message> Button<'a, Message> { /// Creates a new [`Button`] with some local [`State`] and the given /// content. - /// - /// [`Button`]: struct.Button.html - /// [`State`]: struct.State.html pub fn new<E>(_state: &'a mut State, content: E) -> Self where E: Into<Element<'a, Message>>, @@ -58,56 +52,42 @@ impl<'a, Message> Button<'a, Message> { } /// Sets the width of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn width(mut self, width: Length) -> Self { self.width = width; 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 pub fn min_width(mut self, min_width: u32) -> Self { self.min_width = min_width; self } /// Sets the minimum height of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn min_height(mut self, min_height: u32) -> Self { self.min_height = min_height; self } /// Sets the padding of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn padding(mut self, padding: u16) -> Self { self.padding = padding; self } /// Sets the style of the [`Button`]. - /// - /// [`Button`]: struct.Button.html pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { self.style = style.into(); self } /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// [`Button`]: struct.Button.html pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self @@ -115,15 +95,11 @@ impl<'a, Message> Button<'a, Message> { } /// The local state of a [`Button`]. -/// -/// [`Button`]: struct.Button.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State; impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> State { State::default() } @@ -154,16 +130,20 @@ where }, }; + let class = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&padding_class, bump).into_bump_str() + }; + let mut node = button(bump) - .attr( - "class", - bumpalo::format!(in bump, "{}", padding_class).into_bump_str(), - ) + .attr("class", class) .attr( "style", bumpalo::format!( in bump, - "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}", + "background: {}; border-radius: {}px; width:{}; \ + min-width: {}; color: {}", background, style.border_radius, css::length(self.width), diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs index 5ebc26c8..543af99a 100644 --- a/web/src/widget/checkbox.rs +++ b/web/src/widget/checkbox.rs @@ -28,6 +28,7 @@ pub struct Checkbox<Message> { is_checked: bool, on_toggle: Rc<dyn Fn(bool) -> Message>, label: String, + id: Option<String>, width: Length, style: Box<dyn StyleSheet>, } @@ -41,8 +42,6 @@ impl<Message> Checkbox<Message> { /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - /// - /// [`Checkbox`]: struct.Checkbox.html pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where F: 'static + Fn(bool) -> Message, @@ -51,26 +50,29 @@ impl<Message> Checkbox<Message> { is_checked, on_toggle: Rc::new(f), label: label.into(), + id: None, width: Length::Shrink, style: Default::default(), } } /// 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 style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { self.style = style.into(); self } + + /// Sets the id of the [`Checkbox`]. + pub fn id(mut self, id: impl Into<String>) -> Self { + self.id = Some(id.into()); + self + } } impl<Message> Widget<Message> for Checkbox<Message> @@ -84,8 +86,10 @@ where style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; - let checkbox_label = bumpalo::format!(in bump, "{}", self.label); + let checkbox_label = + String::from_str_in(&self.label, bump).into_bump_str(); let event_bus = bus.clone(); let on_toggle = self.on_toggle.clone(); @@ -95,7 +99,15 @@ where let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5)); - label(bump) + let (label, input) = if let Some(id) = &self.id { + let id = String::from_str_in(id, bump).into_bump_str(); + + (label(bump).attr("for", id), input(bump).attr("id", id)) + } else { + (label(bump), input(bump)) + }; + + label .attr( "class", bumpalo::format!(in bump, "{} {}", row_class, spacing_class) @@ -108,7 +120,7 @@ where ) .children(vec![ // TODO: Checkbox styling - input(bump) + input .attr("type", "checkbox") .bool_attr("checked", self.is_checked) .on("click", move |_root, vdom, _event| { @@ -118,8 +130,7 @@ where vdom.schedule_render(); }) .finish(), - span(bump).children(vec![ - text(checkbox_label.into_bump_str())]).finish(), + text(checkbox_label), ]) .finish() } diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs index 25b88b0e..d832fdcb 100644 --- a/web/src/widget/column.rs +++ b/web/src/widget/column.rs @@ -6,8 +6,6 @@ use std::u32; /// A container that distributes its contents vertically. /// /// A [`Column`] will try to fill the horizontal space of its container. -/// -/// [`Column`]: struct.Column.html #[allow(missing_debug_implementations)] pub struct Column<'a, Message> { spacing: u16, @@ -22,15 +20,11 @@ pub struct Column<'a, Message> { impl<'a, Message> Column<'a, Message> { /// Creates an empty [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Column`] with the given elements. - /// - /// [`Column`]: struct.Column.html pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { Column { spacing: 0, @@ -55,56 +49,42 @@ impl<'a, Message> Column<'a, Message> { } /// Sets the padding of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Column`] in pixels. - /// - /// [`Column`]: struct.Column.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Column`] . - /// - /// [`Column`]: struct.Column.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an element to the [`Column`]. - /// - /// [`Column`]: struct.Column.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message>>, diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs index 78be3543..7187a4f0 100644 --- a/web/src/widget/container.rs +++ b/web/src/widget/container.rs @@ -21,8 +21,6 @@ pub struct Container<'a, Message> { impl<'a, Message> Container<'a, Message> { /// Creates an empty [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn new<T>(content: T) -> Self where T: Into<Element<'a, Message>>, @@ -43,48 +41,36 @@ impl<'a, Message> Container<'a, Message> { } /// Sets the padding of the [`Container`]. - /// - /// [`Container`]: struct.Column.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Container`] in pixels. - /// - /// [`Container`]: struct.Container.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; 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; @@ -92,8 +78,6 @@ impl<'a, Message> Container<'a, Message> { } /// Centers the contents in the vertical axis of the [`Container`]. - /// - /// [`Container`]: struct.Container.html pub fn center_y(mut self) -> Self { self.vertical_alignment = Align::Center; @@ -101,8 +85,6 @@ impl<'a, Message> Container<'a, Message> { } /// 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 diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs index 029ab352..05c89ea5 100644 --- a/web/src/widget/image.rs +++ b/web/src/widget/image.rs @@ -22,6 +22,9 @@ pub struct Image { /// The image path pub handle: Handle, + /// The alt text of the image + pub alt: String, + /// The width of the image pub width: Length, @@ -31,31 +34,32 @@ pub struct Image { impl Image { /// Creates a new [`Image`] with the given path. - /// - /// [`Image`]: struct.Image.html pub fn new<T: Into<Handle>>(handle: T) -> Self { Image { handle: handle.into(), + alt: Default::default(), width: Length::Shrink, height: Length::Shrink, } } /// Sets the width of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Image`] boundaries. - /// - /// [`Image`]: struct.Image.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } + + /// Sets the alt text of the [`Image`]. + pub fn alt(mut self, alt: impl Into<String>) -> Self { + self.alt = alt.into(); + self + } } impl<Message> Widget<Message> for Image { @@ -66,12 +70,19 @@ impl<Message> Widget<Message> for Image { _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; - let src = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() { - Data::Path(path) => path.to_str().unwrap_or("") - }); + let src = String::from_str_in( + match self.handle.data.as_ref() { + Data::Path(path) => path.to_str().unwrap_or(""), + }, + bump, + ) + .into_bump_str(); - let mut image = img(bump).attr("src", src.into_bump_str()); + let alt = String::from_str_in(&self.alt, bump).into_bump_str(); + + let mut image = img(bump).attr("src", src).attr("alt", alt); match self.width { Length::Shrink => {} @@ -99,8 +110,6 @@ impl<'a, Message> From<Image> for Element<'a, Message> { } /// An [`Image`] handle. -/// -/// [`Image`]: struct.Image.html #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -109,8 +118,6 @@ pub struct Handle { 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())) } @@ -126,15 +133,11 @@ impl Handle { } /// 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 } @@ -153,8 +156,6 @@ impl From<&str> for Handle { } /// The data of an [`Image`]. -/// -/// [`Image`]: struct.Image.html #[derive(Clone, Hash)] pub enum Data { /// A remote image diff --git a/web/src/widget/progress_bar.rs b/web/src/widget/progress_bar.rs index 856203c0..7d77616e 100644 --- a/web/src/widget/progress_bar.rs +++ b/web/src/widget/progress_bar.rs @@ -32,8 +32,6 @@ impl 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()), @@ -45,24 +43,18 @@ impl ProgressBar { } /// 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 diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs index 520b24cd..5a9bc379 100644 --- a/web/src/widget/radio.rs +++ b/web/src/widget/radio.rs @@ -35,6 +35,8 @@ pub struct Radio<Message> { is_selected: bool, on_click: Message, label: String, + id: Option<String>, + name: Option<String>, style: Box<dyn StyleSheet>, } @@ -47,8 +49,6 @@ impl<Message> Radio<Message> { /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It /// receives the value of the radio and must produce a `Message`. - /// - /// [`Radio`]: struct.Radio.html pub fn new<F, V>( value: V, label: impl Into<String>, @@ -63,17 +63,29 @@ impl<Message> Radio<Message> { is_selected: Some(value) == selected, on_click: f(value), label: label.into(), + id: None, + name: None, style: Default::default(), } } /// Sets the style of the [`Radio`] button. - /// - /// [`Radio`]: struct.Radio.html pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { self.style = style.into(); self } + + /// Sets the name attribute of the [`Radio`] button. + pub fn name(mut self, name: impl Into<String>) -> Self { + self.name = Some(name.into()); + self + } + + /// Sets the id of the [`Radio`] button. + pub fn id(mut self, id: impl Into<String>) -> Self { + self.id = Some(id.into()); + self + } } impl<Message> Widget<Message> for Radio<Message> @@ -87,17 +99,35 @@ where _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; + use dodrio::bumpalo::collections::String; - let radio_label = bumpalo::format!(in bump, "{}", self.label); + let radio_label = + String::from_str_in(&self.label, bump).into_bump_str(); let event_bus = bus.clone(); let on_click = self.on_click.clone(); + let (label, input) = if let Some(id) = &self.id { + let id = String::from_str_in(id, bump).into_bump_str(); + + (label(bump).attr("for", id), input(bump).attr("id", id)) + } else { + (label(bump), input(bump)) + }; + + let input = if let Some(name) = &self.name { + let name = String::from_str_in(name, bump).into_bump_str(); + + dodrio::builder::input(bump).attr("name", name) + } else { + input + }; + // TODO: Complete styling - label(bump) + label .attr("style", "display: block; font-size: 20px") .children(vec![ - input(bump) + input .attr("type", "radio") .attr("style", "margin-right: 10px") .bool_attr("checked", self.is_selected) @@ -105,7 +135,7 @@ where event_bus.publish(on_click.clone()); }) .finish(), - text(radio_label.into_bump_str()), + text(radio_label), ]) .finish() } diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs index cfa10fdf..f00a544a 100644 --- a/web/src/widget/row.rs +++ b/web/src/widget/row.rs @@ -6,8 +6,6 @@ use std::u32; /// A container that distributes its contents horizontally. /// /// A [`Row`] will try to fill the horizontal space of its container. -/// -/// [`Row`]: struct.Row.html #[allow(missing_debug_implementations)] pub struct Row<'a, Message> { spacing: u16, @@ -22,15 +20,11 @@ pub struct Row<'a, Message> { impl<'a, Message> Row<'a, Message> { /// Creates an empty [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn new() -> Self { Self::with_children(Vec::new()) } /// Creates a [`Row`] with the given elements. - /// - /// [`Row`]: struct.Row.html pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { Row { spacing: 0, @@ -55,57 +49,42 @@ impl<'a, Message> Row<'a, Message> { } /// Sets the padding of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the maximum height of the [`Row`]. - /// - /// [`Row`]: struct.Row.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the vertical alignment of the contents of the [`Row`] . - /// - /// [`Row`]: struct.Row.html pub fn align_items(mut self, align: Align) -> Self { self.align_items = align; self } /// Adds an [`Element`] to the [`Row`]. - /// - /// [`Element`]: ../struct.Element.html - /// [`Row`]: struct.Row.html pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message>>, diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs index 07b38aad..f9135dd6 100644 --- a/web/src/widget/scrollable.rs +++ b/web/src/widget/scrollable.rs @@ -16,9 +16,6 @@ pub struct Scrollable<'a, Message> { impl<'a, Message> Scrollable<'a, Message> { /// Creates a new [`Scrollable`] with the given [`State`]. - /// - /// [`Scrollable`]: struct.Scrollable.html - /// [`State`]: struct.State.html pub fn new(_state: &'a mut State) -> Self { use std::u32; @@ -42,64 +39,48 @@ impl<'a, Message> Scrollable<'a, Message> { } /// Sets the padding of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn padding(mut self, units: u16) -> Self { self.content = self.content.padding(units); self } /// Sets the width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the maximum width of the [`Scrollable`]. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_width(mut self, max_width: u32) -> Self { self.content = self.content.max_width(max_width); self } /// Sets the maximum height of the [`Scrollable`] in pixels. - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn max_height(mut self, max_height: u32) -> Self { self.max_height = max_height; self } /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - /// - /// [`Scrollable`]: struct.Scrollable.html pub fn align_items(mut self, align_items: Align) -> Self { self.content = self.content.align_items(align_items); 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 pub fn push<E>(mut self, child: E) -> Self where E: Into<Element<'a, Message>>, @@ -154,15 +135,11 @@ where } /// The local state of a [`Scrollable`]. -/// -/// [`Scrollable`]: struct.Scrollable.html #[derive(Debug, Clone, Copy, Default)] pub struct State; impl State { /// Creates a new [`State`] with the scrollbar located at the top. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs index 5aa6439e..91a4d2ec 100644 --- a/web/src/widget/slider.rs +++ b/web/src/widget/slider.rs @@ -1,9 +1,6 @@ //! 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::{Bus, Css, Element, Length, Widget}; pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; @@ -16,7 +13,8 @@ use std::{ops::RangeInclusive, rc::Rc}; /// /// A [`Slider`] will try to fill the horizontal space of its container. /// -/// [`Slider`]: struct.Slider.html +/// The [`Slider`] range of numeric values is generic and its step size defaults +/// to 1 unit. /// /// # Example /// ``` @@ -34,16 +32,20 @@ use std::{ops::RangeInclusive, rc::Rc}; /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, Message> { +pub struct Slider<'a, T, Message> { _state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, - on_change: Rc<Box<dyn Fn(f32) -> Message>>, + range: RangeInclusive<T>, + step: T, + value: T, + on_change: Rc<Box<dyn Fn(T) -> Message>>, width: Length, style: Box<dyn StyleSheet>, } -impl<'a, Message> Slider<'a, Message> { +impl<'a, T, Message> Slider<'a, T, Message> +where + T: Copy + From<u8> + std::cmp::PartialOrd, +{ /// Creates a new [`Slider`]. /// /// It expects: @@ -53,22 +55,32 @@ impl<'a, Message> Slider<'a, Message> { /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - /// - /// [`Slider`]: struct.Slider.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, - range: RangeInclusive<f32>, - value: f32, + range: RangeInclusive<T>, + value: T, on_change: F, ) -> Self where - F: 'static + Fn(f32) -> Message, + F: 'static + Fn(T) -> Message, { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + Slider { _state: state, - value: value.max(*range.start()).min(*range.end()), + value, range, + step: T::from(1), on_change: Rc::new(Box::new(on_change)), width: Length::Fill, style: Default::default(), @@ -76,24 +88,27 @@ impl<'a, Message> Slider<'a, Message> { } /// Sets the width of the [`Slider`]. - /// - /// [`Slider`]: struct.Slider.html pub fn width(mut self, width: Length) -> Self { 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 } + + /// Sets the step size of the [`Slider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } } -impl<'a, Message> Widget<Message> for Slider<'a, Message> +impl<'a, T, Message> Widget<Message> for Slider<'a, T, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { fn node<'b>( @@ -107,18 +122,18 @@ where let (start, end) = self.range.clone().into_inner(); - let min = bumpalo::format!(in bump, "{}", start); - let max = bumpalo::format!(in bump, "{}", end); - let value = bumpalo::format!(in bump, "{}", self.value); + let min = bumpalo::format!(in bump, "{}", start.into()); + let max = bumpalo::format!(in bump, "{}", end.into()); + let value = bumpalo::format!(in bump, "{}", self.value.into()); + let step = bumpalo::format!(in bump, "{}", self.step.into()); let on_change = self.on_change.clone(); let event_bus = bus.clone(); - // TODO: Make `step` configurable // TODO: Styling input(bump) .attr("type", "range") - .attr("step", "0.01") + .attr("step", step.into_bump_str()) .attr("min", min.into_bump_str()) .attr("max", max.into_bump_str()) .attr("value", value.into_bump_str()) @@ -131,33 +146,32 @@ where Some(slider) => slider, }; - if let Ok(value) = slider.value().parse::<f32>() { - event_bus.publish(on_change(value)); + if let Ok(value) = slider.value().parse::<f64>() { + if let Some(value) = T::from_f64(value) { + event_bus.publish(on_change(value)); + } } }) .finish() } } -impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message> +impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message> where + T: 'static + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'static, { - fn from(slider: Slider<'a, Message>) -> Element<'a, Message> { + fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message> { Element::new(slider) } } /// The local state of a [`Slider`]. -/// -/// [`Slider`]: struct.Slider.html #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State; impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { Self } diff --git a/web/src/widget/space.rs b/web/src/widget/space.rs index 4ce52595..a8571fdb 100644 --- a/web/src/widget/space.rs +++ b/web/src/widget/space.rs @@ -12,15 +12,11 @@ pub struct Space { 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, @@ -29,8 +25,6 @@ impl Space { } /// Creates an amount of vertical [`Space`]. - /// - /// [`Space`]: struct.Space.html pub fn with_height(height: Length) -> Self { Space { width: Length::Shrink, diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs index 3ec565a8..72232dc0 100644 --- a/web/src/widget/text.rs +++ b/web/src/widget/text.rs @@ -28,8 +28,6 @@ pub struct Text { impl Text { /// Create a new fragment of [`Text`] with the given contents. - /// - /// [`Text`]: struct.Text.html pub fn new<T: Into<String>>(label: T) -> Self { Text { content: label.into(), @@ -44,51 +42,36 @@ impl Text { } /// Sets the size of the [`Text`]. - /// - /// [`Text`]: struct.Text.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self } /// Sets the [`Color`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`Color`]: ../../struct.Color.html pub fn color<C: Into<Color>>(mut self, color: C) -> Self { self.color = Some(color.into()); 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 [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Text`] boundaries. - /// - /// [`Text`]: struct.Text.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// Sets the [`HorizontalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html pub fn horizontal_alignment( mut self, alignment: HorizontalAlignment, @@ -98,9 +81,6 @@ impl Text { } /// Sets the [`VerticalAlignment`] of the [`Text`]. - /// - /// [`Text`]: struct.Text.html - /// [`VerticalAlignment`]: enum.VerticalAlignment.html pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self { self.vertical_alignment = alignment; self @@ -116,7 +96,12 @@ impl<'a, Message> Widget<Message> for Text { ) -> dodrio::Node<'b> { use dodrio::builder::*; - let content = bumpalo::format!(in bump, "{}", self.content); + let content = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.content, bump) + }; + let color = self .color .map(css::color) @@ -133,7 +118,8 @@ impl<'a, Message> Widget<Message> for Text { let style = bumpalo::format!( in bump, - "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}", + "width: {}; height: {}; font-size: {}px; color: {}; \ + text-align: {}; font-family: {}", width, height, self.size.unwrap_or(20), diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs index 3fa458bd..bc2048a8 100644 --- a/web/src/widget/text_input.rs +++ b/web/src/widget/text_input.rs @@ -1,9 +1,6 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -//! -//! [`TextInput`]: struct.TextInput.html -//! [`State`]: struct.State.html use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; pub use iced_style::text_input::{Style, StyleSheet}; @@ -53,9 +50,6 @@ impl<'a, Message> TextInput<'a, Message> { /// - a placeholder /// - the current value /// - a function that produces a message when the [`TextInput`] changes - /// - /// [`TextInput`]: struct.TextInput.html - /// [`State`]: struct.State.html pub fn new<F>( state: &'a mut State, placeholder: &str, @@ -81,40 +75,30 @@ impl<'a, Message> TextInput<'a, Message> { } /// 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 pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the maximum width of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn max_width(mut self, max_width: u32) -> Self { self.max_width = max_width; self } /// Sets the padding of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } /// Sets the text size of the [`TextInput`]. - /// - /// [`TextInput`]: struct.TextInput.html pub fn size(mut self, size: u16) -> Self { self.size = Some(size); self @@ -122,16 +106,12 @@ impl<'a, Message> TextInput<'a, Message> { /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - /// - /// [`TextInput`]: struct.TextInput.html pub fn on_submit(mut self, message: Message) -> Self { 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 @@ -151,8 +131,26 @@ where use dodrio::builder::*; use wasm_bindgen::JsCast; - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); + let class = { + use dodrio::bumpalo::collections::String; + + let padding_class = + style_sheet.insert(bump, css::Rule::Padding(self.padding)); + + String::from_str_in(&padding_class, bump).into_bump_str() + }; + + let placeholder = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.placeholder, bump).into_bump_str() + }; + + let value = { + use dodrio::bumpalo::collections::String; + + String::from_str_in(&self.value, bump).into_bump_str() + }; let on_change = self.on_change.clone(); let on_submit = self.on_submit.clone(); @@ -161,15 +159,14 @@ where let style = self.style_sheet.active(); input(bump) - .attr( - "class", - bumpalo::format!(in bump, "{}", padding_class).into_bump_str(), - ) + .attr("class", class) .attr( "style", bumpalo::format!( in bump, - "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}", + "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), @@ -181,19 +178,9 @@ where ) .into_bump_str(), ) - .attr( - "placeholder", - bumpalo::format!(in bump, "{}", self.placeholder) - .into_bump_str(), - ) - .attr( - "value", - bumpalo::format!(in bump, "{}", self.value).into_bump_str(), - ) - .attr( - "type", - bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(), - ) + .attr("placeholder", placeholder) + .attr("value", value) + .attr("type", if self.is_secure { "password" } else { "text" }) .on("input", move |_root, _vdom, event| { let text_input = match event.target().and_then(|t| { t.dyn_into::<web_sys::HtmlInputElement>().ok() @@ -206,10 +193,13 @@ where }) .on("keypress", move |_root, _vdom, event| { if let Some(on_submit) = on_submit.clone() { - let event = event.unchecked_into::<web_sys::KeyboardEvent>(); + let event = + event.unchecked_into::<web_sys::KeyboardEvent>(); match event.key_code() { - 13 => { submit_event_bus.publish(on_submit); } + 13 => { + submit_event_bus.publish(on_submit); + } _ => {} } } @@ -228,22 +218,16 @@ where } /// The state of a [`TextInput`]. -/// -/// [`TextInput`]: struct.TextInput.html #[derive(Debug, Clone, Copy, Default)] pub struct State; impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - /// - /// [`State`]: struct.State.html 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 612af1ed..5f4699a8 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_wgpu" -version = "0.2.2" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A wgpu renderer for Iced" @@ -9,29 +9,31 @@ repository = "https://github.com/hecrj/iced" [features] svg = ["resvg"] -canvas = ["lyon"] +canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] +default_system_font = ["iced_graphics/font-source"] [dependencies] -wgpu = "0.5" -wgpu_glyph = "0.8" -zerocopy = "0.3" -glyph_brush = "0.6" +wgpu = "0.6" +wgpu_glyph = "0.10" +glyph_brush = "0.7" raw-window-handle = "0.3" -glam = "0.8" -font-kit = "0.6" log = "0.4" -guillotiere = "0.5" -# Pin `gfx-memory` until https://github.com/gfx-rs/wgpu-rs/issues/261 is -# resolved -gfx-memory = "=0.1.1" +guillotiere = "0.6" +futures = "0.3" + +[dependencies.bytemuck] +version = "1.4" +features = ["derive"] [dependencies.iced_native] -version = "0.2" +version = "0.3" path = "../native" -[dependencies.iced_style] +[dependencies.iced_graphics] version = "0.1" -path = "../style" +path = "../graphics" +features = ["font-fallback", "font-icons"] [dependencies.image] version = "0.23" @@ -42,10 +44,6 @@ version = "0.9" features = ["raqote-backend"] optional = true -[dependencies.lyon] -version = "0.15" -optional = true - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/wgpu/README.md b/wgpu/README.md index 67cb59dd..e8cb0a43 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -15,7 +15,9 @@ Currently, `iced_wgpu` supports the following primitives: - Images and SVG, loaded from memory or the file system. - Meshes of triangles, useful to draw geometry freely. - +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> [documentation]: https://docs.rs/iced_wgpu [`iced_native`]: ../native @@ -27,7 +29,7 @@ Currently, `iced_wgpu` supports the following primitives: Add `iced_wgpu` as a dependency in your `Cargo.toml`: ```toml -iced_wgpu = "0.2" +iced_wgpu = "0.3" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs new file mode 100644 index 00000000..fccb5ac7 --- /dev/null +++ b/wgpu/src/backend.rs @@ -0,0 +1,288 @@ +use crate::quad; +use crate::text; +use crate::triangle; +use crate::{Settings, Transformation}; +use iced_graphics::backend; +use iced_graphics::font; +use iced_graphics::layer::Layer; +use iced_graphics::{Primitive, Viewport}; +use iced_native::mouse; +use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment}; + +#[cfg(any(feature = "image", feature = "svg"))] +use crate::image; + +/// A [`wgpu`] graphics backend for [`iced`]. +/// +/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +/// [`iced`]: https://github.com/hecrj/iced +#[derive(Debug)] +pub struct Backend { + quad_pipeline: quad::Pipeline, + text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: image::Pipeline, + + default_text_size: u16, +} + +impl Backend { + /// Creates a new [`Backend`]. + pub fn new(device: &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, + ); + + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = image::Pipeline::new(device, settings.format); + + Self { + quad_pipeline, + text_pipeline, + triangle_pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + + default_text_size: settings.default_text_size, + } + } + + /// Draws the provided primitives in the given `TextureView`. + /// + /// The text provided as overlay will be rendered on top of the primitives. + /// This is useful for rendering debug information. + pub fn draw<T: AsRef<str>>( + &mut self, + device: &wgpu::Device, + staging_belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + viewport: &Viewport, + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), + overlay_text: &[T], + ) -> mouse::Interaction { + log::debug!("Drawing"); + + let target_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let transformation = viewport.projection(); + + let mut layers = Layer::generate(primitive, viewport); + layers.push(Layer::overlay(overlay_text, viewport)); + + for layer in layers { + self.flush( + device, + scale_factor, + transformation, + &layer, + staging_belt, + encoder, + &frame, + target_size.width, + target_size.height, + ); + } + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.trim_cache(); + + *mouse_interaction + } + + fn flush( + &mut self, + device: &wgpu::Device, + scale_factor: f32, + transformation: Transformation, + layer: &Layer<'_>, + staging_belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_width: u32, + target_height: u32, + ) { + let bounds = (layer.bounds * scale_factor).snap(); + + if !layer.quads.is_empty() { + self.quad_pipeline.draw( + device, + staging_belt, + encoder, + &layer.quads, + transformation, + scale_factor, + bounds, + target, + ); + } + + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.draw( + device, + staging_belt, + encoder, + target, + target_width, + target_height, + scaled, + scale_factor, + &layer.meshes, + ); + } + + #[cfg(any(feature = "image", feature = "svg"))] + { + if !layer.images.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.image_pipeline.draw( + device, + staging_belt, + encoder, + &layer.images, + scaled, + bounds, + target, + scale_factor, + ); + } + } + + if !layer.text.is_empty() { + for text in layer.text.iter() { + // Target physical coordinates directly to avoid blurry text + let text = wgpu_glyph::Section { + // TODO: We `round` here to avoid rerasterizing text when + // its position changes slightly. This can make text feel a + // bit "jumpy". We may be able to do better once we improve + // our text rendering/caching pipeline. + screen_position: ( + (text.bounds.x * scale_factor).round(), + (text.bounds.y * scale_factor).round(), + ), + // 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 + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. + bounds: ( + (text.bounds.width * scale_factor).ceil(), + (text.bounds.height * scale_factor).ceil(), + ), + text: vec![wgpu_glyph::Text { + text: text.content, + scale: wgpu_glyph::ab_glyph::PxScale { + x: text.size * scale_factor, + y: text.size * scale_factor, + }, + font_id: self.text_pipeline.find_font(text.font), + extra: wgpu_glyph::Extra { + color: text.color, + z: 0.0, + }, + }], + layout: wgpu_glyph::Layout::default() + .h_align(match text.horizontal_alignment { + HorizontalAlignment::Left => { + wgpu_glyph::HorizontalAlign::Left + } + HorizontalAlignment::Center => { + wgpu_glyph::HorizontalAlign::Center + } + HorizontalAlignment::Right => { + wgpu_glyph::HorizontalAlign::Right + } + }) + .v_align(match text.vertical_alignment { + VerticalAlignment::Top => { + wgpu_glyph::VerticalAlign::Top + } + VerticalAlignment::Center => { + wgpu_glyph::VerticalAlign::Center + } + VerticalAlignment::Bottom => { + wgpu_glyph::VerticalAlign::Bottom + } + }), + ..Default::default() + }; + + self.text_pipeline.queue(text); + } + + self.text_pipeline.draw_queued( + device, + staging_belt, + encoder, + target, + transformation, + wgpu_glyph::Region { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + ); + } + } +} + +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + self.text_pipeline.trim_measurement_cache() + } +} + +impl backend::Text for Backend { + const ICON_FONT: Font = font::ICONS; + const CHECKMARK_ICON: char = font::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; + + fn default_size(&self) -> u16 { + self.default_text_size + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.text_pipeline.measure(contents, size, font, bounds) + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, handle: &iced_native::image::Handle) -> (u32, u32) { + self.image_pipeline.dimensions(handle) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + handle: &iced_native::svg::Handle, + ) -> (u32, u32) { + self.image_pipeline.viewport_dimensions(handle) + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index ea5dc09d..c256ca7e 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -9,10 +9,12 @@ mod vector; use crate::Transformation; use atlas::Atlas; +use iced_graphics::layer; use iced_native::Rectangle; use std::cell::RefCell; use std::mem; -use zerocopy::AsBytes; + +use bytemuck::{Pod, Zeroable}; #[cfg(feature = "image")] use iced_native::image; @@ -41,6 +43,8 @@ pub struct Pipeline { impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + use wgpu::util::DeviceExt; + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -48,50 +52,52 @@ impl Pipeline { 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: wgpu::CompareFunction::Always, + ..Default::default() }); let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[ + label: Some("iced_wgpu::image constants layout"), + entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::VERTEX, - ty: wgpu::BindingType::UniformBuffer { dynamic: false }, + ty: wgpu::BindingType::UniformBuffer { + dynamic: false, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::<Uniforms>() as u64, + ), + }, + count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { comparison: false }, + count: None, }, ], }); - let uniforms = Uniforms { - transform: Transformation::identity().into(), - }; - - let uniforms_buffer = device.create_buffer_with_data( - uniforms.as_bytes(), - wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, - ); + let uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + mapped_at_creation: false, + }); let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::image constants bind group"), layout: &constant_layout, - bindings: &[ - wgpu::Binding { + entries: &[ + wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &uniforms_buffer, - range: 0..std::mem::size_of::<Uniforms>() as u64, - }, + resource: wgpu::BindingResource::Buffer( + uniforms_buffer.slice(..), + ), }, - wgpu::Binding { + wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, @@ -100,8 +106,8 @@ impl Pipeline { let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[wgpu::BindGroupLayoutEntry { + label: Some("iced_wgpu::image texture atlas layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::SampledTexture { @@ -109,29 +115,29 @@ impl Pipeline { component_type: wgpu::TextureComponentType::Float, multisampled: false, }, + count: None, }], }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::image pipeline layout"), + push_constant_ranges: &[], bind_group_layouts: &[&constant_layout, &texture_layout], }); - let vs = include_bytes!("shader/image.vert.spv"); - let vs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) - .expect("Read image vertex shader as SPIR-V"), - ); + let vs_module = device.create_shader_module(wgpu::include_spirv!( + "shader/image.vert.spv" + )); - let fs = include_bytes!("shader/image.frag.spv"); - let fs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) - .expect("Read image fragment shader as SPIR-V"), - ); + let fs_module = device.create_shader_module(wgpu::include_spirv!( + "shader/image.frag.spv" + )); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &layout, + label: Some("iced_wgpu::image pipeline"), + layout: Some(&layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -143,9 +149,7 @@ impl Pipeline { 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, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -213,28 +217,33 @@ impl Pipeline { alpha_to_coverage_enabled: false, }); - let vertices = device.create_buffer_with_data( - QUAD_VERTS.as_bytes(), - wgpu::BufferUsage::VERTEX, - ); + let vertices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced_wgpu::image vertex buffer"), + contents: bytemuck::cast_slice(&QUAD_VERTS), + usage: wgpu::BufferUsage::VERTEX, + }); - let indices = device.create_buffer_with_data( - QUAD_INDICES.as_bytes(), - wgpu::BufferUsage::INDEX, - ); + let indices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced_wgpu::image index buffer"), + contents: bytemuck::cast_slice(&QUAD_INDICES), + usage: wgpu::BufferUsage::INDEX, + }); let instances = device.create_buffer(&wgpu::BufferDescriptor { - label: None, + label: Some("iced_wgpu::image instance buffer"), size: mem::size_of::<Instance>() as u64 * Instance::MAX as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, + mapped_at_creation: false, }); let texture_atlas = Atlas::new(device); let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::image texture atlas bind group"), layout: &texture_layout, - bindings: &[wgpu::Binding { + entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView( &texture_atlas.view(), @@ -281,8 +290,9 @@ impl Pipeline { pub fn draw( &mut self, device: &wgpu::Device, + staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, - images: &[Image], + images: &[layer::Image], transformation: Transformation, bounds: Rectangle<u32>, target: &wgpu::TextureView, @@ -297,31 +307,48 @@ impl Pipeline { let mut vector_cache = self.vector_cache.borrow_mut(); for image in images { - match &image.handle { + match &image { #[cfg(feature = "image")] - Handle::Raster(handle) => { + layer::Image::Raster { handle, bounds } => { if let Some(atlas_entry) = raster_cache.upload( handle, device, encoder, &mut self.texture_atlas, ) { - add_instances(image, atlas_entry, instances); + add_instances( + [bounds.x, bounds.y], + [bounds.width, bounds.height], + atlas_entry, + instances, + ); } } + #[cfg(not(feature = "image"))] + layer::Image::Raster { .. } => {} + #[cfg(feature = "svg")] - Handle::Vector(handle) => { + layer::Image::Vector { handle, bounds } => { + let size = [bounds.width, bounds.height]; + if let Some(atlas_entry) = vector_cache.upload( handle, - image.size, + size, _scale, device, encoder, &mut self.texture_atlas, ) { - add_instances(image, atlas_entry, instances); + add_instances( + [bounds.x, bounds.y], + size, + atlas_entry, + instances, + ); } } + #[cfg(not(feature = "svg"))] + layer::Image::Vector { .. } => {} } } @@ -336,9 +363,9 @@ impl Pipeline { self.texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::image texture atlas bind group"), layout: &self.texture_layout, - bindings: &[wgpu::Binding { + entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView( &self.texture_atlas.view(), @@ -349,26 +376,20 @@ impl Pipeline { self.texture_version = texture_version; } - let uniforms_buffer = device.create_buffer_with_data( - Uniforms { + { + let mut uniforms_buffer = staging_belt.write_buffer( + encoder, + &self.uniforms, + 0, + wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64) + .unwrap(), + device, + ); + + uniforms_buffer.copy_from_slice(bytemuck::bytes_of(&Uniforms { transform: transformation.into(), - } - .as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); - - encoder.copy_buffer_to_buffer( - &uniforms_buffer, - 0, - &self.uniforms, - 0, - std::mem::size_of::<Uniforms>() as u64, - ); - - let instances_buffer = device.create_buffer_with_data( - instances.as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); + })); + } let mut i = 0; let total = instances.len(); @@ -377,27 +398,30 @@ impl Pipeline { 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, + let mut instances_buffer = staging_belt.write_buffer( + encoder, &self.instances, 0, - (amount * std::mem::size_of::<Instance>()) as u64, + wgpu::BufferSize::new( + (amount * std::mem::size_of::<Instance>()) as u64, + ) + .unwrap(), + device, ); + instances_buffer.copy_from_slice(bytemuck::cast_slice( + &instances[i..i + amount], + )); + 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, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, }, ], @@ -407,9 +431,9 @@ impl Pipeline { 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, 0); - render_pass.set_vertex_buffer(0, &self.vertices, 0, 0); - render_pass.set_vertex_buffer(1, &self.instances, 0, 0); + render_pass.set_index_buffer(self.indices.slice(..)); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); + render_pass.set_vertex_buffer(1, self.instances.slice(..)); render_pass.set_scissor_rect( bounds.x, @@ -437,22 +461,8 @@ impl Pipeline { } } -pub struct Image { - pub handle: Handle, - pub position: [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, AsBytes)] +#[derive(Clone, Copy, Zeroable, Pod)] pub struct Vertex { _position: [f32; 2], } @@ -475,7 +485,7 @@ const QUAD_VERTS: [Vertex; 4] = [ ]; #[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], _size: [f32; 2], @@ -489,28 +499,29 @@ impl Instance { } #[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], } fn add_instances( - image: &Image, + image_position: [f32; 2], + image_size: [f32; 2], entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image.position, image.size, allocation, instances); + 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; + 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 [x, y] = image_position; let (fragment_x, fragment_y) = fragment.position; let (fragment_width, fragment_height) = allocation.size(); diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 3ce3ce8b..660ebe44 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -28,9 +28,8 @@ impl Atlas { }; let texture = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label: Some("iced_wgpu::image texture atlas"), size: extent, - array_layer_count: 2, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, @@ -40,12 +39,15 @@ impl Atlas { | wgpu::TextureUsage::SAMPLED, }); - let texture_view = texture.create_default_view(); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); Atlas { texture, texture_view, - layers: vec![Layer::Empty, Layer::Empty], + layers: vec![Layer::Empty], } } @@ -65,6 +67,8 @@ impl Atlas { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, ) -> Option<Entry> { + use wgpu::util::DeviceExt; + let entry = { let current_size = self.layers.len(); let entry = self.allocate(width, height)?; @@ -78,8 +82,31 @@ impl Atlas { log::info!("Allocated atlas entry: {:?}", entry); + // It is a webgpu requirement that: + // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 + // So we calculate padded_width by rounding width up to the next + // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padding = (align - (4 * width) % align) % align; + let padded_width = (4 * width + padding) as usize; + let padded_data_size = padded_width * height as usize; + + let mut padded_data = vec![0; padded_data_size]; + + for row in 0..height as usize { + let offset = row * padded_width; + + padded_data[offset..offset + 4 * width as usize].copy_from_slice( + &data[row * 4 * width as usize..(row + 1) * 4 * width as usize], + ) + } + let buffer = - device.create_buffer_with_data(data, wgpu::BufferUsage::COPY_SRC); + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced_wgpu::image staging buffer"), + contents: &padded_data, + usage: wgpu::BufferUsage::COPY_SRC, + }); match &entry { Entry::Contiguous(allocation) => { @@ -87,6 +114,7 @@ impl Atlas { &buffer, width, height, + padding, 0, &allocation, encoder, @@ -95,12 +123,13 @@ impl Atlas { Entry::Fragmented { fragments, .. } => { for fragment in fragments { let (x, y) = fragment.position; - let offset = (y * width + x) as usize * 4; + let offset = (y * padded_width as u32 + 4 * x) as usize; self.upload_allocation( &buffer, width, height, + padding, offset, &fragment.allocation, encoder, @@ -253,6 +282,7 @@ impl Atlas { buffer: &wgpu::Buffer, image_width: u32, image_height: u32, + padding: u32, offset: usize, allocation: &Allocation, encoder: &mut wgpu::CommandEncoder, @@ -270,15 +300,20 @@ impl Atlas { encoder.copy_buffer_to_texture( wgpu::BufferCopyView { buffer, - offset: offset as u64, - bytes_per_row: 4 * image_width, - rows_per_image: image_height, + layout: wgpu::TextureDataLayout { + offset: offset as u64, + bytes_per_row: 4 * image_width + padding, + rows_per_image: image_height, + }, }, wgpu::TextureCopyView { texture: &self.texture, - array_layer: layer as u32, mip_level: 0, - origin: wgpu::Origin3d { x, y, z: 0 }, + origin: wgpu::Origin3d { + x, + y, + z: layer as u32, + }, }, extent, ); @@ -295,13 +330,12 @@ impl Atlas { } let new_texture = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label: Some("iced_wgpu::image texture atlas"), size: wgpu::Extent3d { width: SIZE, height: SIZE, - depth: 1, + depth: self.layers.len() as u32, }, - array_layer_count: self.layers.len() as u32, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, @@ -323,15 +357,21 @@ impl Atlas { encoder.copy_texture_to_texture( wgpu::TextureCopyView { texture: &self.texture, - array_layer: i as u32, mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: i as u32, + }, }, wgpu::TextureCopyView { texture: &new_texture, - array_layer: i as u32, mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: i as u32, + }, }, wgpu::Extent3d { width: SIZE, @@ -342,6 +382,10 @@ impl Atlas { } self.texture = new_texture; - self.texture_view = self.texture.create_default_view(); + self.texture_view = + self.texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); } } diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 4f69df8c..25607dab 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -43,14 +43,14 @@ impl Cache { let memory = match handle.data() { image::Data::Path(path) => { if let Ok(image) = ::image::open(path) { - Memory::Host(image.to_bgra()) + Memory::Host(image.to_bgra8()) } else { Memory::NotFound } } image::Data::Bytes(bytes) => { if let Ok(image) = ::image::load_from_memory(&bytes) { - Memory::Host(image.to_bgra()) + Memory::Host(image.to_bgra8()) } else { Memory::Invalid } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 7648aa7e..95df2e99 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -2,8 +2,6 @@ use crate::image::atlas::{self, Atlas}; use iced_native::svg; use std::collections::{HashMap, HashSet}; -use zerocopy::AsBytes; - pub enum Svg { Loaded(resvg::usvg::Tree), NotFound, @@ -119,7 +117,7 @@ impl Cache { let allocation = texture_atlas.upload( width, height, - canvas.get_data().as_bytes(), + bytemuck::cast_slice(canvas.get_data()), device, encoder, )?; diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 799c1f34..a4c2ac0e 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -1,6 +1,6 @@ //! A [`wgpu`] renderer for [`iced_native`]. //! -//!  +//!  //! //! For now, it is the default renderer of [Iced] in native platforms. //! @@ -23,38 +23,37 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![forbid(unsafe_code)] +#![deny(unsafe_code)] #![forbid(rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg))] -pub mod defaults; pub mod settings; pub mod triangle; pub mod widget; pub mod window; -mod primitive; +mod backend; mod quad; -mod renderer; -mod target; mod text; -mod transformation; -mod viewport; +pub use iced_graphics::{ + Antialiasing, Color, Defaults, Error, Primitive, Viewport, +}; pub use wgpu; -pub use defaults::Defaults; -pub use primitive::Primitive; -pub use renderer::Renderer; +pub use backend::Backend; pub use settings::Settings; -pub use target::Target; -pub use viewport::Viewport; #[doc(no_inline)] pub use widget::*; -pub(crate) use quad::Quad; -pub(crate) use transformation::Transformation; +pub(crate) use iced_graphics::Transformation; #[cfg(any(feature = "image", feature = "svg"))] mod image; + +/// A [`wgpu`] graphics renderer for [`iced`]. +/// +/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +/// [`iced`]: https://github.com/hecrj/iced +pub type Renderer = iced_graphics::Renderer<Backend>; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 0c2d2244..24d20cfa 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,8 +1,10 @@ use crate::Transformation; +use iced_graphics::layer; use iced_native::Rectangle; +use bytemuck::{Pod, Zeroable}; use std::mem; -use zerocopy::AsBytes; +use wgpu::util::DeviceExt; #[derive(Debug)] pub struct Pipeline { @@ -18,51 +20,55 @@ impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline { let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[wgpu::BindGroupLayoutEntry { + label: Some("iced_wgpu::quad uniforms layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::VERTEX, - ty: wgpu::BindingType::UniformBuffer { dynamic: false }, + ty: wgpu::BindingType::UniformBuffer { + dynamic: false, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::<Uniforms>() as u64, + ), + }, + count: None, }], }); - let constants_buffer = device.create_buffer_with_data( - Uniforms::default().as_bytes(), - wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, - ); + let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::quad uniforms buffer"), + size: mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + mapped_at_creation: false, + }); let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::quad uniforms bind group"), layout: &constant_layout, - bindings: &[wgpu::Binding { + entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &constants_buffer, - range: 0..std::mem::size_of::<Uniforms>() as u64, - }, + resource: wgpu::BindingResource::Buffer( + constants_buffer.slice(..), + ), }], }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::quad pipeline layout"), + push_constant_ranges: &[], bind_group_layouts: &[&constant_layout], }); - let vs = include_bytes!("shader/quad.vert.spv"); - let vs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) - .expect("Read quad vertex shader as SPIR-V"), - ); + let vs_module = device + .create_shader_module(wgpu::include_spirv!("shader/quad.vert.spv")); - let fs = include_bytes!("shader/quad.frag.spv"); - let fs_module = device.create_shader_module( - &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) - .expect("Read quad fragment shader as SPIR-V"), - ); + let fs_module = device + .create_shader_module(wgpu::include_spirv!("shader/quad.frag.spv")); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &layout, + label: Some("iced_wgpu::quad pipeline"), + layout: Some(&layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -74,9 +80,7 @@ impl Pipeline { 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, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -107,7 +111,7 @@ impl Pipeline { }], }, wgpu::VertexBufferDescriptor { - stride: mem::size_of::<Quad>() as u64, + stride: mem::size_of::<layer::Quad>() as u64, step_mode: wgpu::InputStepMode::Instance, attributes: &[ wgpu::VertexAttributeDescriptor { @@ -149,20 +153,25 @@ impl Pipeline { alpha_to_coverage_enabled: false, }); - let vertices = device.create_buffer_with_data( - QUAD_VERTS.as_bytes(), - wgpu::BufferUsage::VERTEX, - ); + let vertices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced_wgpu::quad vertex buffer"), + contents: bytemuck::cast_slice(&QUAD_VERTS), + usage: wgpu::BufferUsage::VERTEX, + }); - let indices = device.create_buffer_with_data( - QUAD_INDICES.as_bytes(), - wgpu::BufferUsage::INDEX, - ); + let indices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced_wgpu::quad index buffer"), + contents: bytemuck::cast_slice(&QUAD_INDICES), + usage: wgpu::BufferUsage::INDEX, + }); let instances = device.create_buffer(&wgpu::BufferDescriptor { - label: None, - size: mem::size_of::<Quad>() as u64 * Quad::MAX as u64, + label: Some("iced_wgpu::quad instance buffer"), + size: mem::size_of::<layer::Quad>() as u64 * MAX_INSTANCES as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, + mapped_at_creation: false, }); Pipeline { @@ -178,8 +187,9 @@ impl Pipeline { pub fn draw( &mut self, device: &wgpu::Device, + staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, - instances: &[Quad], + instances: &[layer::Quad], transformation: Transformation, scale: f32, bounds: Rectangle<u32>, @@ -187,39 +197,38 @@ impl Pipeline { ) { let uniforms = Uniforms::new(transformation, scale); - let constants_buffer = device.create_buffer_with_data( - uniforms.as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); + { + let mut constants_buffer = staging_belt.write_buffer( + encoder, + &self.constants_buffer, + 0, + wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64) + .unwrap(), + device, + ); - encoder.copy_buffer_to_buffer( - &constants_buffer, - 0, - &self.constants_buffer, - 0, - std::mem::size_of::<Uniforms>() as u64, - ); + constants_buffer.copy_from_slice(bytemuck::bytes_of(&uniforms)); + } let mut i = 0; let total = instances.len(); while i < total { - let end = (i + Quad::MAX).min(total); + let end = (i + MAX_INSTANCES).min(total); let amount = end - i; - let instance_buffer = device.create_buffer_with_data( - &instances[i..end].as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); + let instance_bytes = bytemuck::cast_slice(&instances[i..end]); - encoder.copy_buffer_to_buffer( - &instance_buffer, - 0, + let mut instance_buffer = staging_belt.write_buffer( + encoder, &self.instances, 0, - (mem::size_of::<Quad>() * amount) as u64, + wgpu::BufferSize::new(instance_bytes.len() as u64).unwrap(), + device, ); + instance_buffer.copy_from_slice(instance_bytes); + { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -227,13 +236,9 @@ impl Pipeline { 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, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, }, ], @@ -242,9 +247,9 @@ impl Pipeline { render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_index_buffer(&self.indices, 0, 0); - render_pass.set_vertex_buffer(0, &self.vertices, 0, 0); - render_pass.set_vertex_buffer(1, &self.instances, 0, 0); + render_pass.set_index_buffer(self.indices.slice(..)); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); + render_pass.set_vertex_buffer(1, self.instances.slice(..)); render_pass.set_scissor_rect( bounds.x, bounds.y, @@ -260,13 +265,13 @@ impl Pipeline { ); } - i += Quad::MAX; + i += MAX_INSTANCES; } } } #[repr(C)] -#[derive(Clone, Copy, AsBytes)] +#[derive(Clone, Copy, Zeroable, Pod)] pub struct Vertex { _position: [f32; 2], } @@ -288,23 +293,10 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -#[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] -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 { - const MAX: usize = 100_000; -} +const MAX_INSTANCES: usize = 100_000; #[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], scale: f32, diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs deleted file mode 100644 index 71b4af38..00000000 --- a/wgpu/src/renderer.rs +++ /dev/null @@ -1,507 +0,0 @@ -use crate::{ - quad, text, triangle, Defaults, Primitive, Quad, Settings, Target, - Transformation, -}; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image::{self, Image}; - -use iced_native::{ - layout, mouse, Background, Color, Layout, Point, Rectangle, Vector, Widget, -}; - -mod widget; - -/// A [`wgpu`] renderer. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -#[derive(Debug)] -pub struct Renderer { - quad_pipeline: quad::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>, - quads: Vec<Quad>, - meshes: Vec<(Vector, Rectangle<u32>, &'a triangle::Mesh2D)>, - text: Vec<wgpu_glyph::Section<'a>>, - - #[cfg(any(feature = "image", feature = "svg"))] - images: Vec<Image>, -} - -impl<'a> Layer<'a> { - pub fn new(bounds: Rectangle<u32>) -> Self { - Self { - bounds, - quads: Vec::new(), - text: Vec::new(), - meshes: Vec::new(), - - #[cfg(any(feature = "image", feature = "svg"))] - images: Vec::new(), - } - } - - pub fn intersection(&self, rectangle: Rectangle) -> Option<Rectangle<u32>> { - let layer_bounds: Rectangle<f32> = self.bounds.into(); - - layer_bounds.intersection(&rectangle).map(Into::into) - } -} - -impl Renderer { - /// Creates a new [`Renderer`]. - /// - /// [`Renderer`]: struct.Renderer.html - pub fn new(device: &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, - ); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = image::Pipeline::new(device, settings.format); - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - } - } - - /// 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: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: Target<'_>, - (primitive, mouse_interaction): &(Primitive, mouse::Interaction), - scale_factor: f64, - overlay: &[T], - ) -> mouse::Interaction { - log::debug!("Drawing"); - - let (width, height) = target.viewport.dimensions(); - let scale_factor = scale_factor as f32; - let transformation = target.viewport.transformation(); - - let mut layers = Vec::new(); - - layers.push(Layer::new(Rectangle { - x: 0, - y: 0, - width, - height, - })); - - self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers); - self.draw_overlay(overlay, &mut layers); - - for layer in layers { - self.flush( - device, - scale_factor, - transformation, - &layer, - encoder, - target.texture, - width, - height, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.trim_cache(); - - *mouse_interaction - } - - fn draw_primitive<'a>( - &mut self, - translation: Vector, - primitive: &'a Primitive, - layers: &mut Vec<Layer<'a>>, - ) { - match primitive { - Primitive::None => {} - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - self.draw_primitive(translation, primitive, layers) - } - } - Primitive::Text { - content, - bounds, - size, - color, - font, - horizontal_alignment, - vertical_alignment, - } => { - let layer = layers.last_mut().unwrap(); - - layer.text.push(wgpu_glyph::Section { - text: &content, - screen_position: ( - bounds.x + translation.x, - bounds.y + translation.y, - ), - bounds: (bounds.width, bounds.height), - scale: wgpu_glyph::Scale { x: *size, y: *size }, - color: color.into_linear(), - font_id: self.text_pipeline.find_font(*font), - layout: wgpu_glyph::Layout::default() - .h_align(match horizontal_alignment { - iced_native::HorizontalAlignment::Left => { - wgpu_glyph::HorizontalAlign::Left - } - iced_native::HorizontalAlignment::Center => { - wgpu_glyph::HorizontalAlign::Center - } - iced_native::HorizontalAlignment::Right => { - wgpu_glyph::HorizontalAlign::Right - } - }) - .v_align(match vertical_alignment { - iced_native::VerticalAlignment::Top => { - wgpu_glyph::VerticalAlign::Top - } - iced_native::VerticalAlignment::Center => { - wgpu_glyph::VerticalAlign::Center - } - iced_native::VerticalAlignment::Bottom => { - wgpu_glyph::VerticalAlign::Bottom - } - }), - ..Default::default() - }) - } - Primitive::Quad { - bounds, - background, - border_radius, - border_width, - border_color, - } => { - let layer = layers.last_mut().unwrap(); - - // TODO: Move some of these computations to the GPU (?) - layer.quads.push(Quad { - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - scale: [bounds.width, bounds.height], - color: match background { - 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::Mesh2D { size, buffers } => { - let layer = layers.last_mut().unwrap(); - - // Only draw visible content - if let Some(clip_bounds) = layer.intersection(Rectangle::new( - Point::new(translation.x, translation.y), - *size, - )) { - layer.meshes.push(( - translation, - clip_bounds.into(), - buffers, - )); - } - } - Primitive::Clip { - bounds, - offset, - content, - } => { - let layer = layers.last_mut().unwrap(); - - // Only draw visible content - if let Some(clip_bounds) = - layer.intersection(*bounds + translation) - { - let clip_layer = Layer::new(clip_bounds.into()); - let new_layer = Layer::new(layer.bounds); - - layers.push(clip_layer); - self.draw_primitive( - translation - - Vector::new(offset.x as f32, offset.y as f32), - content, - layers, - ); - layers.push(new_layer); - } - } - Primitive::Translate { - translation: new_translation, - content, - } => { - self.draw_primitive( - translation + *new_translation, - &content, - layers, - ); - } - - Primitive::Cached { cache } => { - self.draw_primitive(translation, &cache, layers); - } - - #[cfg(feature = "image")] - Primitive::Image { handle, bounds } => { - let layer = layers.last_mut().unwrap(); - - layer.images.push(Image { - handle: image::Handle::Raster(handle.clone()), - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - }); - } - #[cfg(not(feature = "image"))] - Primitive::Image { .. } => {} - - #[cfg(feature = "svg")] - Primitive::Svg { handle, bounds } => { - let layer = layers.last_mut().unwrap(); - - layer.images.push(Image { - handle: image::Handle::Vector(handle.clone()), - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - }); - } - #[cfg(not(feature = "svg"))] - Primitive::Svg { .. } => {} - } - } - - fn draw_overlay<'a, T: AsRef<str>>( - &mut self, - lines: &'a [T], - layers: &mut Vec<Layer<'a>>, - ) { - let first = layers.first().unwrap(); - let mut overlay = Layer::new(first.bounds); - - let font_id = self.text_pipeline.overlay_font(); - let scale = wgpu_glyph::Scale { x: 20.0, y: 20.0 }; - - for (i, line) in lines.iter().enumerate() { - overlay.text.push(wgpu_glyph::Section { - text: line.as_ref(), - screen_position: (11.0, 11.0 + 25.0 * i as f32), - color: [0.9, 0.9, 0.9, 1.0], - scale, - font_id, - ..wgpu_glyph::Section::default() - }); - - overlay.text.push(wgpu_glyph::Section { - text: line.as_ref(), - screen_position: (10.0, 10.0 + 25.0 * i as f32), - color: [0.0, 0.0, 0.0, 1.0], - scale, - font_id, - ..wgpu_glyph::Section::default() - }); - } - - layers.push(overlay); - } - - fn flush( - &mut self, - device: &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 * scale_factor; - - if !layer.quads.is_empty() { - self.quad_pipeline.draw( - device, - encoder, - &layer.quads, - transformation, - scale_factor, - bounds, - target, - ); - } - - if !layer.meshes.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.triangle_pipeline.draw( - device, - encoder, - target, - target_width, - target_height, - scaled, - scale_factor, - &layer.meshes, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.image_pipeline.draw( - device, - encoder, - &layer.images, - scaled, - bounds, - target, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - for text in layer.text.iter() { - // Target physical coordinates directly to avoid blurry text - let text = wgpu_glyph::Section { - // TODO: We `round` here to avoid rerasterizing text when - // its position changes slightly. This can make text feel a - // bit "jumpy". We may be able to do better once we improve - // our text rendering/caching pipeline. - screen_position: ( - (text.screen_position.0 * scale_factor).round(), - (text.screen_position.1 * scale_factor).round(), - ), - // 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 - // scaling when rendering. This would ensure that both - // measuring and rendering follow the same layout rules. - bounds: ( - (text.bounds.0 * scale_factor).ceil(), - (text.bounds.1 * scale_factor).ceil(), - ), - scale: wgpu_glyph::Scale { - x: text.scale.x * scale_factor, - y: text.scale.y * scale_factor, - }, - ..*text - }; - - self.text_pipeline.queue(text); - } - - self.text_pipeline.draw_queued( - device, - encoder, - target, - transformation, - wgpu_glyph::Region { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - ); - } - } -} - -impl iced_native::Renderer for Renderer { - type Output = (Primitive, mouse::Interaction); - 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, limits); - - self.text_pipeline.clear_measurement_cache(); - - node - } -} - -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, defaults, layout, cursor_position); - - explain_layout(layout, color, &mut primitives); - primitives.push(primitive); - - (Primitive::Group { primitives }, cursor) - } -} - -fn explain_layout( - layout: Layout<'_>, - color: Color, - primitives: &mut Vec<Primitive>, -) { - primitives.push(Primitive::Quad { - bounds: layout.bounds(), - 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() { - explain_layout(child, color, primitives); - } -} diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs deleted file mode 100644 index 37421fbe..00000000 --- a/wgpu/src/renderer/widget.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod button; -mod checkbox; -mod column; -mod container; -mod pane_grid; -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/container.rs b/wgpu/src/renderer/widget/container.rs deleted file mode 100644 index 30cc3f07..00000000 --- a/wgpu/src/renderer/widget/container.rs +++ /dev/null @@ -1,48 +0,0 @@ -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), - }, - }; - - let (content, mouse_interaction) = - 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_interaction, - ) - } else { - (content, mouse_interaction) - } - } -} diff --git a/wgpu/src/renderer/widget/pane_grid.rs b/wgpu/src/renderer/widget/pane_grid.rs deleted file mode 100644 index 2253e4af..00000000 --- a/wgpu/src/renderer/widget/pane_grid.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{ - mouse, - pane_grid::{self, Axis, Pane}, - Element, Layout, Point, Rectangle, Vector, -}; - -impl pane_grid::Renderer for Renderer { - fn draw<Message>( - &mut self, - defaults: &Self::Defaults, - content: &[(Pane, Element<'_, Message, Self>)], - dragging: Option<Pane>, - resizing: Option<Axis>, - layout: Layout<'_>, - cursor_position: Point, - ) -> Self::Output { - let pane_cursor_position = if dragging.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - let mut mouse_interaction = mouse::Interaction::default(); - let mut dragged_pane = None; - - let mut panes: Vec<_> = content - .iter() - .zip(layout.children()) - .enumerate() - .map(|(i, ((id, pane), layout))| { - let (primitive, new_mouse_interaction) = - pane.draw(self, defaults, layout, pane_cursor_position); - - if new_mouse_interaction > mouse_interaction { - mouse_interaction = new_mouse_interaction; - } - - if Some(*id) == dragging { - dragged_pane = Some((i, layout)); - } - - primitive - }) - .collect(); - - let primitives = if let Some((index, layout)) = dragged_pane { - let pane = panes.remove(index); - let bounds = layout.bounds(); - - // TODO: Fix once proper layering is implemented. - // This is a pretty hacky way to achieve layering. - let clip = Primitive::Clip { - bounds: Rectangle { - x: cursor_position.x - bounds.width / 2.0, - y: cursor_position.y - bounds.height / 2.0, - width: bounds.width + 0.5, - height: bounds.height + 0.5, - }, - offset: Vector::new(0, 0), - content: Box::new(Primitive::Translate { - translation: Vector::new( - cursor_position.x - bounds.x - bounds.width / 2.0, - cursor_position.y - bounds.y - bounds.height / 2.0, - ), - content: Box::new(pane), - }), - }; - - panes.push(clip); - - panes - } else { - panes - }; - - ( - Primitive::Group { primitives }, - if dragging.is_some() { - mouse::Interaction::Grabbing - } else if let Some(axis) = resizing { - match axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - } - } else { - mouse_interaction - }, - ) - } -} diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs deleted file mode 100644 index 225f7e6c..00000000 --- a/wgpu/src/renderer/widget/space.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::{Primitive, Renderer}; -use iced_native::{mouse, space, Rectangle}; - -impl space::Renderer for Renderer { - fn draw(&mut self, _bounds: Rectangle) -> Self::Output { - (Primitive::None, mouse::Interaction::default()) - } -} diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 5ee245b6..26763e22 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,22 +1,31 @@ -//! Configure a [`Renderer`]. -//! -//! [`Renderer`]: struct.Renderer.html +//! Configure a renderer. +pub use crate::Antialiasing; -/// The settings of a [`Renderer`]. +/// The settings of a [`Backend`]. /// -/// [`Renderer`]: ../struct.Renderer.html +/// [`Backend`]: crate::Backend #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Settings { - /// The output format of the [`Renderer`]. + /// The output format of the [`Backend`]. /// - /// [`Renderer`]: ../struct.Renderer.html + /// [`Backend`]: crate::Backend pub format: wgpu::TextureFormat, + /// The present mode of the [`Backend`]. + /// + /// [`Backend`]: crate::Backend + pub present_mode: wgpu::PresentMode, + /// 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 default size of text. + /// + /// By default, it will be set to 20. + pub default_text_size: u16, + /// The antialiasing strategy that will be used for triangle primitives. pub antialiasing: Option<Antialiasing>, } @@ -25,32 +34,10 @@ impl Default for Settings { fn default() -> Settings { Settings { format: wgpu::TextureFormat::Bgra8UnormSrgb, + present_mode: wgpu::PresentMode::Mailbox, default_font: None, + default_text_size: 20, 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/quad.vert b/wgpu/src/shader/quad.vert index 1d9a4fd2..09a278b1 100644 --- a/wgpu/src/shader/quad.vert +++ b/wgpu/src/shader/quad.vert @@ -24,6 +24,11 @@ void main() { vec2 p_Pos = i_Pos * u_Scale; vec2 p_Scale = i_Scale * u_Scale; + float i_BorderRadius = min( + i_BorderRadius, + min(i_Scale.x, i_Scale.y) / 2.0 + ); + mat4 i_Transform = mat4( vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv Binary files differindex 7059b51b..fa71ba1e 100644 --- a/wgpu/src/shader/quad.vert.spv +++ b/wgpu/src/shader/quad.vert.spv diff --git a/wgpu/src/target.rs b/wgpu/src/target.rs deleted file mode 100644 index 1e72c0c3..00000000 --- a/wgpu/src/target.rs +++ /dev/null @@ -1,14 +0,0 @@ -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 f4521e72..78999cf8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,24 +1,13 @@ -mod font; - use crate::Transformation; - +use iced_graphics::font; use std::{cell::RefCell, collections::HashMap}; - -pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External { - name: "iced_wgpu icons", - bytes: include_bytes!("text/icons.ttf"), -}; - -pub const CHECKMARK_ICON: char = '\u{F00C}'; - -const FALLBACK_FONT: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); +use wgpu_glyph::ab_glyph; #[derive(Debug)] pub struct Pipeline { - draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>, + draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>, draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>, - - measure_brush: RefCell<glyph_brush::GlyphBrush<'static, ()>>, + measure_brush: RefCell<glyph_brush::GlyphBrush<()>>, } impl Pipeline { @@ -27,51 +16,48 @@ impl Pipeline { format: wgpu::TextureFormat, default_font: Option<&[u8]>, ) -> Self { - // TODO: Font customization - let font_source = font::Source::new(); + let default_font = default_font.map(|slice| slice.to_vec()); - let default_font = - default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| { - font_source + // TODO: Font customization + #[cfg(feature = "default_system_font")] + let default_font = { + default_font.or_else(|| { + font::Source::new() .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(), - )) + .ok() + }) }; - 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 default_font = + default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); - load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font") + let font = ab_glyph::FontArc::try_from_vec(default_font) + .unwrap_or_else(|_| { + log::warn!( + "System font failed to load. Falling back to \ + embedded font..." + ); + + ab_glyph::FontArc::try_from_slice(font::FALLBACK) + .expect("Load fallback font") }); - let draw_brush = brush_builder - .initial_cache_size((2048, 2048)) - .build(device, format); + let draw_brush = + wgpu_glyph::GlyphBrushBuilder::using_font(font.clone()) + .initial_cache_size((2048, 2048)) + .draw_cache_multithread(false) // TODO: Expose as a configuration flag + .build(device, format); + + let measure_brush = + glyph_brush::GlyphBrushBuilder::using_font(font).build(); Pipeline { draw_brush: RefCell::new(draw_brush), draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), } } - pub fn overlay_font(&self) -> wgpu_glyph::FontId { - wgpu_glyph::FontId(0) - } - pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) { self.draw_brush.borrow_mut().queue(section); } @@ -79,6 +65,7 @@ impl Pipeline { pub fn draw_queued( &mut self, device: &wgpu::Device, + staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, transformation: Transformation, @@ -88,6 +75,7 @@ impl Pipeline { .borrow_mut() .draw_queued_with_transform_and_scissoring( device, + staging_belt, encoder, target, transformation.into(), @@ -108,10 +96,13 @@ impl Pipeline { let wgpu_glyph::FontId(font_id) = self.find_font(font); let section = wgpu_glyph::Section { - text: content, - scale: wgpu_glyph::Scale { x: size, y: size }, bounds: (bounds.width, bounds.height), - font_id: wgpu_glyph::FontId(font_id), + text: vec![wgpu_glyph::Text { + text: content, + scale: size.into(), + font_id: wgpu_glyph::FontId(font_id), + extra: wgpu_glyph::Extra::default(), + }], ..Default::default() }; @@ -124,21 +115,7 @@ impl Pipeline { } } - pub fn space_width(&self, size: f32) -> f32 { - use wgpu_glyph::GlyphCruncher; - - let glyph_brush = self.measure_brush.borrow(); - - // TODO: Select appropriate font - let font = &glyph_brush.fonts()[0]; - - font.glyph(' ') - .scaled(wgpu_glyph::Scale { x: size, y: size }) - .h_metrics() - .advance_width - } - - pub fn clear_measurement_cache(&mut self) { + pub fn trim_measurement_cache(&mut self) { // TODO: We should probably use a `GlyphCalculator` for this. However, // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. // This makes stuff quite inconvenient. A manual method for trimming the @@ -170,11 +147,12 @@ impl Pipeline { return *font_id; } - // TODO: Find a way to share font data - let _ = self.measure_brush.borrow_mut().add_font_bytes(bytes); + let font = ab_glyph::FontArc::try_from_slice(bytes) + .expect("Load font"); + + let _ = self.measure_brush.borrow_mut().add_font(font.clone()); - let font_id = - self.draw_brush.borrow_mut().add_font_bytes(bytes); + let font_id = self.draw_brush.borrow_mut().add_font(font); let _ = self .draw_font_map diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 3e68a269..61a771d8 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,12 +1,15 @@ //! Draw meshes of triangles. use crate::{settings, Transformation}; -use iced_native::{Rectangle, Vector}; +use iced_graphics::layer; + +use bytemuck::{Pod, Zeroable}; use std::mem; -use zerocopy::AsBytes; + +pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; mod msaa; -const UNIFORM_BUFFER_SIZE: usize = 100; +const UNIFORM_BUFFER_SIZE: usize = 50; const VERTEX_BUFFER_SIZE: usize = 10_000; const INDEX_BUFFER_SIZE: usize = 10_000; @@ -14,6 +17,7 @@ const INDEX_BUFFER_SIZE: usize = 10_000; pub(crate) struct Pipeline { pipeline: wgpu::RenderPipeline, blit: Option<msaa::Blit>, + constants_layout: wgpu::BindGroupLayout, constants: wgpu::BindGroup, uniforms_buffer: Buffer<Uniforms>, vertex_buffer: Buffer<Vertex2D>, @@ -22,6 +26,7 @@ pub(crate) struct Pipeline { #[derive(Debug)] struct Buffer<T> { + label: &'static str, raw: wgpu::Buffer, size: usize, usage: wgpu::BufferUsage, @@ -30,17 +35,20 @@ struct Buffer<T> { impl<T> Buffer<T> { pub fn new( + label: &'static str, device: &wgpu::Device, size: usize, usage: wgpu::BufferUsage, ) -> Self { let raw = device.create_buffer(&wgpu::BufferDescriptor { - label: None, + label: Some(label), size: (std::mem::size_of::<T>() * size) as u64, usage, + mapped_at_creation: false, }); Buffer { + label, raw, size, usage, @@ -48,16 +56,21 @@ impl<T> Buffer<T> { } } - pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) { - if self.size < size { + pub fn expand(&mut self, device: &wgpu::Device, size: usize) -> bool { + let needs_resize = self.size < size; + + if needs_resize { self.raw = device.create_buffer(&wgpu::BufferDescriptor { - label: None, + label: Some(self.label), size: (std::mem::size_of::<T>() * size) as u64, usage: self.usage, + mapped_at_creation: false, }); self.size = size; } + + needs_resize } } @@ -67,17 +80,24 @@ impl Pipeline { format: wgpu::TextureFormat, antialiasing: Option<settings::Antialiasing>, ) -> Pipeline { - let constant_layout = + let constants_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[wgpu::BindGroupLayoutEntry { + label: Some("iced_wgpu::triangle uniforms layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::VERTEX, - ty: wgpu::BindingType::UniformBuffer { dynamic: true }, + ty: wgpu::BindingType::UniformBuffer { + dynamic: true, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::<Uniforms>() as u64, + ), + }, + count: None, }], }); let constants_buffer = Buffer::new( + "iced_wgpu::triangle uniforms buffer", device, UNIFORM_BUFFER_SIZE, wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, @@ -85,37 +105,37 @@ impl Pipeline { let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &constant_layout, - bindings: &[wgpu::Binding { + label: Some("iced_wgpu::triangle uniforms bind group"), + layout: &constants_layout, + entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &constants_buffer.raw, - range: 0..std::mem::size_of::<Uniforms>() as u64, - }, + resource: wgpu::BindingResource::Buffer( + constants_buffer + .raw + .slice(0..std::mem::size_of::<Uniforms>() as u64), + ), }], }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - bind_group_layouts: &[&constant_layout], + label: Some("iced_wgpu::triangle pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&constants_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 vs_module = device.create_shader_module(wgpu::include_spirv!( + "shader/triangle.vert.spv" + )); - 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 fs_module = device.create_shader_module(wgpu::include_spirv!( + "shader/triangle.frag.spv" + )); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &layout, + label: Some("iced_wgpu::triangle pipeline"), + layout: Some(&layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -127,9 +147,7 @@ impl Pipeline { 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, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -168,9 +186,9 @@ impl Pipeline { ], }], }, - sample_count: antialiasing - .map(|a| a.sample_count()) - .unwrap_or(1), + sample_count: u32::from( + antialiasing.map(|a| a.sample_count()).unwrap_or(1), + ), sample_mask: !0, alpha_to_coverage_enabled: false, }); @@ -178,14 +196,17 @@ impl Pipeline { Pipeline { pipeline, blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + constants_layout, constants: constant_bind_group, uniforms_buffer: constants_buffer, vertex_buffer: Buffer::new( + "iced_wgpu::triangle vertex buffer", device, VERTEX_BUFFER_SIZE, wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, ), index_buffer: Buffer::new( + "iced_wgpu::triangle index buffer", device, INDEX_BUFFER_SIZE, wgpu::BufferUsage::INDEX | wgpu::BufferUsage::COPY_DST, @@ -196,29 +217,49 @@ impl Pipeline { pub fn draw( &mut self, device: &wgpu::Device, + staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, target_width: u32, target_height: u32, transformation: Transformation, scale_factor: f32, - meshes: &[(Vector, Rectangle<u32>, &Mesh2D)], + meshes: &[layer::Mesh<'_>], ) { // 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())) + .map(|layer::Mesh { buffers, .. }| { + (buffers.vertices.len(), buffers.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 _ = self.vertex_buffer.expand(device, total_vertices); + let _ = self.index_buffer.expand(device, total_indices); + + // If the uniforms buffer is resized, then we need to recreate its + // bind group. + if self.uniforms_buffer.expand(device, meshes.len()) { + self.constants = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle uniforms buffer"), + layout: &self.constants_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + self.uniforms_buffer.raw.slice( + 0..std::mem::size_of::<Uniforms>() as u64, + ), + ), + }], + }); + } let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len()); let mut offsets: Vec<( @@ -230,68 +271,85 @@ impl Pipeline { let mut last_index = 0; // We upload everything upfront - for (origin, _, mesh) in meshes { + for mesh in meshes { let transform = (transformation - * Transformation::translate(origin.x, origin.y)) + * Transformation::translate(mesh.origin.x, mesh.origin.y)) .into(); - let vertex_buffer = device.create_buffer_with_data( - mesh.vertices.as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); + let vertices = bytemuck::cast_slice(&mesh.buffers.vertices); + let indices = bytemuck::cast_slice(&mesh.buffers.indices); + + match ( + wgpu::BufferSize::new(vertices.len() as u64), + wgpu::BufferSize::new(indices.len() as u64), + ) { + (Some(vertices_size), Some(indices_size)) => { + { + let mut vertex_buffer = staging_belt.write_buffer( + encoder, + &self.vertex_buffer.raw, + (std::mem::size_of::<Vertex2D>() * last_vertex) + as u64, + vertices_size, + device, + ); + + vertex_buffer.copy_from_slice(vertices); + } + + { + let mut index_buffer = staging_belt.write_buffer( + encoder, + &self.index_buffer.raw, + (std::mem::size_of::<u32>() * last_index) as u64, + indices_size, + device, + ); + + index_buffer.copy_from_slice(indices); + } + + uniforms.push(transform); + offsets.push(( + last_vertex as u64, + last_index as u64, + mesh.buffers.indices.len(), + )); + + last_vertex += mesh.buffers.vertices.len(); + last_index += mesh.buffers.indices.len(); + } + _ => {} + } + } - let index_buffer = device.create_buffer_with_data( - mesh.indices.as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); + let uniforms = bytemuck::cast_slice(&uniforms); - 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, + if let Some(uniforms_size) = + wgpu::BufferSize::new(uniforms.len() as u64) + { + let mut uniforms_buffer = staging_belt.write_buffer( + encoder, + &self.uniforms_buffer.raw, 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_size, + device, ); - 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(); + uniforms_buffer.copy_from_slice(uniforms); } - let uniforms_buffer = device.create_buffer_with_data( - uniforms.as_bytes(), - wgpu::BufferUsage::COPY_SRC, - ); - - 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) = + let (attachment, resolve_target, load) = 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) + ( + attachment, + Some(resolve_target), + wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + ) } else { (target, None, wgpu::LoadOp::Load) }; @@ -302,14 +360,7 @@ impl Pipeline { 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, - }, + ops: wgpu::Operations { load, store: true }, }, ], depth_stencil_attachment: None, @@ -320,13 +371,13 @@ impl Pipeline { for (i, (vertex_offset, index_offset, indices)) in offsets.into_iter().enumerate() { - let bounds = meshes[i].1 * scale_factor; + let clip_bounds = (meshes[i].clip_bounds * scale_factor).snap(); render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, ); render_pass.set_bind_group( @@ -336,16 +387,16 @@ impl Pipeline { ); render_pass.set_index_buffer( - &self.index_buffer.raw, - index_offset * std::mem::size_of::<u32>() as u64, - 0, + self.index_buffer + .raw + .slice(index_offset * mem::size_of::<u32>() as u64..), ); render_pass.set_vertex_buffer( 0, - &self.vertex_buffer.raw, - vertex_offset * std::mem::size_of::<Vertex2D>() as u64, - 0, + self.vertex_buffer.raw.slice( + vertex_offset * mem::size_of::<Vertex2D>() as u64.., + ), ); render_pass.draw_indexed(0..indices as u32, 0, 0..1); @@ -359,7 +410,7 @@ impl Pipeline { } #[repr(C)] -#[derive(Debug, Clone, Copy, AsBytes)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], // We need to align this to 256 bytes to please `wgpu`... @@ -387,26 +438,3 @@ impl From<Transformation> for Uniforms { } } } - -/// A two-dimensional vertex with some color in __linear__ RGBA. -#[repr(C)] -#[derive(Copy, Clone, Debug, AsBytes)] -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 index f52c888b..db86f748 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -23,26 +23,25 @@ impl Blit { 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: wgpu::CompareFunction::Always, + ..Default::default() }); let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[wgpu::BindGroupLayoutEntry { + label: Some("iced_wgpu::triangle:msaa uniforms layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { comparison: false }, + count: None, }], }); let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::triangle::msaa uniforms bind group"), layout: &constant_layout, - bindings: &[wgpu::Binding { + entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(&sampler), }], @@ -50,8 +49,8 @@ impl Blit { let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - bindings: &[wgpu::BindGroupLayoutEntry { + label: Some("iced_wgpu::triangle::msaa texture layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::SampledTexture { @@ -59,29 +58,29 @@ impl Blit { component_type: wgpu::TextureComponentType::Float, multisampled: false, }, + count: None, }], }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::triangle::msaa pipeline layout"), + push_constant_ranges: &[], 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 vs_module = device.create_shader_module(wgpu::include_spirv!( + "../shader/blit.vert.spv" + )); - 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 fs_module = device.create_shader_module(wgpu::include_spirv!( + "../shader/blit.frag.spv" + )); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - layout: &layout, + label: Some("iced_wgpu::triangle::msaa pipeline"), + layout: Some(&layout), vertex_stage: wgpu::ProgrammableStageDescriptor { module: &vs_module, entry_point: "main", @@ -93,9 +92,7 @@ impl Blit { 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, + ..Default::default() }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { @@ -179,13 +176,9 @@ impl Blit { 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, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, }, ], @@ -228,9 +221,8 @@ impl Targets { }; let attachment = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label: Some("iced_wgpu::triangle::msaa attachment"), size: extent, - array_layer_count: 1, mip_level_count: 1, sample_count, dimension: wgpu::TextureDimension::D2, @@ -239,9 +231,8 @@ impl Targets { }); let resolve = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label: Some("iced_wgpu::triangle::msaa resolve target"), size: extent, - array_layer_count: 1, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, @@ -250,13 +241,16 @@ impl Targets { | wgpu::TextureUsage::SAMPLED, }); - let attachment = attachment.create_default_view(); - let resolve = resolve.create_default_view(); + let attachment = + attachment.create_view(&wgpu::TextureViewDescriptor::default()); + + let resolve = + resolve.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("iced_wgpu::triangle::msaa texture bind group"), layout: texture_layout, - bindings: &[wgpu::Binding { + entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&resolve), }], diff --git a/wgpu/src/viewport.rs b/wgpu/src/viewport.rs deleted file mode 100644 index 66242468..00000000 --- a/wgpu/src/viewport.rs +++ /dev/null @@ -1,29 +0,0 @@ -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 index 32ccad17..177ae1b6 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -7,18 +7,20 @@ //! ``` //! use iced_wgpu::{button, Button}; //! ``` +use crate::Renderer; + pub mod button; pub mod checkbox; pub mod container; pub mod pane_grid; +pub mod pick_list; pub mod progress_bar; pub mod radio; +pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; -mod text; - #[doc(no_inline)] pub use button::Button; #[doc(no_inline)] @@ -28,18 +30,20 @@ pub use container::Container; #[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; #[doc(no_inline)] +pub use rule::Rule; +#[doc(no_inline)] pub use scrollable::Scrollable; #[doc(no_inline)] pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; -pub use text::Text; - #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub mod canvas; @@ -47,3 +51,22 @@ pub mod canvas; #[cfg(feature = "canvas")] #[doc(no_inline)] pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + +pub use iced_native::Space; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>; + +/// A paragraph of text. +pub type Text = iced_native::Text<Renderer>; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs index b738c55e..fc729cd5 100644 --- a/wgpu/src/widget/button.rs +++ b/wgpu/src/widget/button.rs @@ -1,13 +1,10 @@ //! 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_graphics::button::{Style, StyleSheet}; pub use iced_native::button::State; -pub use iced_style::button::{Style, StyleSheet}; /// A widget that produces a message when clicked. /// diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs index 2fc10ea0..399dd19c 100644 --- a/wgpu/src/widget/canvas.rs +++ b/wgpu/src/widget/canvas.rs @@ -3,231 +3,4 @@ //! 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, mouse, Clipboard, Element, Hasher, Layout, Length, Point, Size, - Vector, Widget, -}; -use std::hash::Hash; -use std::marker::PhantomData; - -pub mod path; - -mod cache; -mod cursor; -mod event; -mod fill; -mod frame; -mod geometry; -mod program; -mod stroke; -mod text; - -pub use cache::Cache; -pub use cursor::Cursor; -pub use event::Event; -pub use fill::Fill; -pub use frame::Frame; -pub use geometry::Geometry; -pub use path::Path; -pub use program::Program; -pub use stroke::{LineCap, LineJoin, Stroke}; -pub use text::Text; - -/// A widget capable of drawing 2D graphics. -/// -/// [`Canvas`]: struct.Canvas.html -/// -/// # Examples -/// The repository has a couple of [examples] showcasing how to use a -/// [`Canvas`]: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`game_of_life`], an interactive version of the Game of Life, invented by -/// John Conway. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// -/// [examples]: https://github.com/hecrj/iced/tree/master/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock -/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life -/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub use iced_wgpu::canvas; -/// # pub use iced_native::{Color, Rectangle}; -/// # } -/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ -/// // We prepare a new `Frame` -/// let mut frame = Frame::new(bounds.size()); -/// -/// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); -/// -/// // And fill it with some color -/// frame.fill(&circle, Fill::Color(Color::BLACK)); -/// -/// // Finally, we produce the geometry -/// vec![frame.into_geometry()] -/// } -/// } -/// -/// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); -/// ``` -#[derive(Debug)] -pub struct Canvas<Message, P: Program<Message>> { - width: Length, - height: Length, - program: P, - phantom: PhantomData<Message>, -} - -impl<Message, P: Program<Message>> Canvas<Message, P> { - const DEFAULT_SIZE: u16 = 100; - - /// Creates a new [`Canvas`]. - /// - /// [`Canvas`]: struct.Canvas.html - pub fn new(program: P) -> Self { - Canvas { - width: Length::Units(Self::DEFAULT_SIZE), - height: Length::Units(Self::DEFAULT_SIZE), - program, - phantom: PhantomData, - } - } - - /// 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 - } -} - -impl<Message, P: Program<Message>> Widget<Message, Renderer> - for Canvas<Message, P> -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec<Message>, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { - let bounds = layout.bounds(); - - let canvas_event = match event { - iced_native::Event::Mouse(mouse_event) => { - Some(Event::Mouse(mouse_event)) - } - _ => None, - }; - - let cursor = Cursor::from_window_position(cursor_position); - - if let Some(canvas_event) = canvas_event { - if let Some(message) = - self.program.update(canvas_event, bounds, cursor) - { - messages.push(message); - } - } - } - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, mouse::Interaction) { - let bounds = layout.bounds(); - let translation = Vector::new(bounds.x, bounds.y); - let cursor = Cursor::from_window_position(cursor_position); - - ( - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { - primitives: self - .program - .draw(bounds, cursor) - .into_iter() - .map(Geometry::into_primitive) - .collect(), - }), - }, - self.program.mouse_interaction(bounds, cursor), - ) - } - - fn hash_layout(&self, state: &mut Hasher) { - struct Marker; - std::any::TypeId::of::<Marker>().hash(state); - - self.width.hash(state); - self.height.hash(state); - } -} - -impl<'a, Message, P: Program<Message> + 'a> From<Canvas<Message, P>> - for Element<'a, Message, Renderer> -where - Message: 'static, -{ - fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} +pub use iced_graphics::canvas::*; diff --git a/wgpu/src/widget/canvas/event.rs b/wgpu/src/widget/canvas/event.rs deleted file mode 100644 index ad11f51e..00000000 --- a/wgpu/src/widget/canvas/event.rs +++ /dev/null @@ -1,10 +0,0 @@ -use iced_native::mouse; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: struct.Event.html -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), -} diff --git a/wgpu/src/widget/canvas/fill.rs b/wgpu/src/widget/canvas/fill.rs deleted file mode 100644 index a2010e45..00000000 --- a/wgpu/src/widget/canvas/fill.rs +++ /dev/null @@ -1,20 +0,0 @@ -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) - } -} - -impl From<Color> for Fill { - fn from(color: Color) -> Fill { - Fill::Color(color) - } -} diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs index da0d7a84..d27d77cc 100644 --- a/wgpu/src/widget/checkbox.rs +++ b/wgpu/src/widget/checkbox.rs @@ -1,7 +1,7 @@ //! Show toggle controls using checkboxes. use crate::Renderer; -pub use iced_style::checkbox::{Style, StyleSheet}; +pub use iced_graphics::checkbox::{Style, StyleSheet}; /// A box that can be checked. /// diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs index 9a93a246..bc26cef2 100644 --- a/wgpu/src/widget/container.rs +++ b/wgpu/src/widget/container.rs @@ -1,7 +1,7 @@ //! Decorate content and apply alignment. use crate::Renderer; -pub use iced_style::container::{Style, StyleSheet}; +pub use iced_graphics::container::{Style, StyleSheet}; /// An element decorating some content. /// diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs index 6f437df7..c26dde48 100644 --- a/wgpu/src/widget/pane_grid.rs +++ b/wgpu/src/widget/pane_grid.rs @@ -6,13 +6,12 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.1/examples/pane_grid -//! [`PaneGrid`]: type.PaneGrid.html +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid use crate::Renderer; pub use iced_native::pane_grid::{ - Axis, Content, Direction, DragEvent, Focus, KeyPressEvent, Node, Pane, - ResizeEvent, Split, State, + Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split, + State, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -22,3 +21,11 @@ pub use iced_native::pane_grid::{ /// /// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>; + +/// The content of a [`Pane`]. +pub type Content<'a, Message> = + iced_native::pane_grid::Content<'a, Message, Renderer>; + +/// The title bar of a [`Pane`]. +pub type TitleBar<'a, Message> = + iced_native::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/wgpu/src/widget/pick_list.rs b/wgpu/src/widget/pick_list.rs new file mode 100644 index 00000000..fccc68c9 --- /dev/null +++ b/wgpu/src/widget/pick_list.rs @@ -0,0 +1,9 @@ +//! Display a dropdown list of selectable values. +pub use iced_native::pick_list::State; + +pub use iced_graphics::overlay::menu::Style as Menu; +pub use iced_graphics::pick_list::{Style, StyleSheet}; + +/// A widget allowing the selection of a single value from a list of options. +pub type PickList<'a, T, Message> = + iced_native::PickList<'a, T, Message, crate::Renderer>; diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs index 770bcea8..45a25d00 100644 --- a/wgpu/src/widget/progress_bar.rs +++ b/wgpu/src/widget/progress_bar.rs @@ -2,11 +2,9 @@ //! //! A [`ProgressBar`] has a range of possible values and a current value, //! as well as a length, height and style. -//! -//! [`ProgressBar`]: type.ProgressBar.html use crate::Renderer; -pub use iced_style::progress_bar::{Style, StyleSheet}; +pub use iced_graphics::progress_bar::{Style, StyleSheet}; /// A bar that displays progress. /// diff --git a/wgpu/src/widget/qr_code.rs b/wgpu/src/widget/qr_code.rs new file mode 100644 index 00000000..7b1c2408 --- /dev/null +++ b/wgpu/src/widget/qr_code.rs @@ -0,0 +1,2 @@ +//! Encode and display information in a QR code. +pub use iced_graphics::qr_code::*; diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs index 6e5cf042..0b843d1f 100644 --- a/wgpu/src/widget/radio.rs +++ b/wgpu/src/widget/radio.rs @@ -1,7 +1,7 @@ //! Create choices using radio buttons. use crate::Renderer; -pub use iced_style::radio::{Style, StyleSheet}; +pub use iced_graphics::radio::{Style, StyleSheet}; /// A circular button representing a choice. /// diff --git a/wgpu/src/widget/rule.rs b/wgpu/src/widget/rule.rs new file mode 100644 index 00000000..3f7bc67a --- /dev/null +++ b/wgpu/src/widget/rule.rs @@ -0,0 +1,10 @@ +//! Display a horizontal or vertical rule for dividing content. + +use crate::Renderer; + +pub use iced_graphics::rule::{FillMode, Style, StyleSheet}; + +/// Display a horizontal or vertical rule for dividing content. +/// +/// This is an alias of an `iced_native` rule with an `iced_wgpu::Renderer`. +pub type Rule = iced_native::Rule<Renderer>; diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs index 1d236105..fabb4318 100644 --- a/wgpu/src/widget/scrollable.rs +++ b/wgpu/src/widget/scrollable.rs @@ -1,8 +1,8 @@ //! Navigate an endless amount of content with a scrollbar. use crate::Renderer; +pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; 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. diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs index 4e47978f..9a269858 100644 --- a/wgpu/src/widget/slider.rs +++ b/wgpu/src/widget/slider.rs @@ -1,16 +1,13 @@ //! 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_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; 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>; +pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>; diff --git a/wgpu/src/widget/text.rs b/wgpu/src/widget/text.rs deleted file mode 100644 index 1053ea97..00000000 --- a/wgpu/src/widget/text.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Write some text for your users to read. -use crate::Renderer; - -/// A paragraph of text. -/// -/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. -pub type Text = iced_native::Text<Renderer>; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs index 260fe3a6..db18b1cc 100644 --- a/wgpu/src/widget/text_input.rs +++ b/wgpu/src/widget/text_input.rs @@ -1,13 +1,10 @@ //! 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_graphics::text_input::{Style, StyleSheet}; pub use iced_native::text_input::State; -pub use iced_style::text_input::{Style, StyleSheet}; /// A field that can be filled with text. /// diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index b7adad82..aac5fb9e 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,6 +1,4 @@ //! Display rendering results on windows. -mod backend; -mod swap_chain; +mod compositor; -pub use backend::Backend; -pub use swap_chain::SwapChain; +pub use compositor::Compositor; diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs deleted file mode 100644 index 2924ce5d..00000000 --- a/wgpu/src/window/backend.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{window::SwapChain, Renderer, Settings, Target}; - -use iced_native::{futures, mouse}; -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 (mut device, queue) = futures::executor::block_on(async { - let adapter = wgpu::Adapter::request( - &wgpu::RequestAdapterOptions { - power_preference: if settings.antialiasing.is_none() { - wgpu::PowerPreference::Default - } else { - wgpu::PowerPreference::HighPerformance - }, - compatible_surface: None, - }, - wgpu::BackendBit::PRIMARY, - ) - .await - .expect("Request adapter"); - - adapter - .request_device(&wgpu::DeviceDescriptor { - extensions: wgpu::Extensions { - anisotropic_filtering: false, - }, - limits: wgpu::Limits { max_bind_groups: 2 }, - }) - .await - }); - - 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], - ) -> mouse::Interaction { - let (frame, viewport) = swap_chain.next_frame().expect("Next frame"); - - let mut encoder = self.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); - - 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_interaction = renderer.draw( - &mut self.device, - &mut encoder, - Target { - texture: &frame.view, - viewport, - }, - output, - scale_factor, - overlay, - ); - - self.queue.submit(&[encoder.finish()]); - - mouse_interaction - } -} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs new file mode 100644 index 00000000..492efb42 --- /dev/null +++ b/wgpu/src/window/compositor.rs @@ -0,0 +1,177 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use futures::task::SpawnExt; +use iced_native::{futures, mouse}; +use raw_window_handle::HasRawWindowHandle; + +/// A window graphics backend for iced powered by `wgpu`. +#[allow(missing_debug_implementations)] +pub struct Compositor { + settings: Settings, + instance: wgpu::Instance, + device: wgpu::Device, + queue: wgpu::Queue, + staging_belt: wgpu::util::StagingBelt, + local_pool: futures::executor::LocalPool, +} + +impl Compositor { + const CHUNK_SIZE: u64 = 10 * 1024; + + /// Requests a new [`Compositor`] with the given [`Settings`]. + /// + /// Returns `None` if no compatible graphics adapter could be found. + pub async fn request(settings: Settings) -> Option<Self> { + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: if settings.antialiasing.is_none() { + wgpu::PowerPreference::Default + } else { + wgpu::PowerPreference::HighPerformance + }, + compatible_surface: None, + }) + .await?; + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits { + max_bind_groups: 2, + ..wgpu::Limits::default() + }, + shader_validation: false, + }, + None, + ) + .await + .ok()?; + + let staging_belt = wgpu::util::StagingBelt::new(Self::CHUNK_SIZE); + let local_pool = futures::executor::LocalPool::new(); + + Some(Compositor { + instance, + settings, + device, + queue, + staging_belt, + local_pool, + }) + } + + /// Creates a new rendering [`Backend`] for this [`Compositor`]. + pub fn create_backend(&self) -> Backend { + Backend::new(&self.device, self.settings) + } +} + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = wgpu::Surface; + type SwapChain = wgpu::SwapChain; + + fn new(settings: Self::Settings) -> Result<(Self, Renderer), Error> { + let compositor = futures::executor::block_on(Self::request(settings)) + .ok_or(Error::AdapterNotFound)?; + + let backend = compositor.create_backend(); + + Ok((compositor, Renderer::new(backend))) + } + + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> wgpu::Surface { + #[allow(unsafe_code)] + unsafe { + self.instance.create_surface(window) + } + } + + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> Self::SwapChain { + self.device.create_swap_chain( + surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: self.settings.format, + present_mode: self.settings.present_mode, + width, + height, + }, + ) + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut Self::SwapChain, + viewport: &Viewport, + background_color: Color, + output: &<Self::Renderer as iced_native::Renderer>::Output, + overlay: &[T], + ) -> mouse::Interaction { + let frame = swap_chain.get_current_frame().expect("Next frame"); + + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.output.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear({ + let [r, g, b, a] = background_color.into_linear(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + let mouse_interaction = renderer.backend_mut().draw( + &mut self.device, + &mut self.staging_belt, + &mut encoder, + &frame.output.view, + viewport, + output, + overlay, + ); + + // Submit work + self.staging_belt.finish(); + self.queue.submit(Some(encoder.finish())); + + // Recall staging buffers + self.local_pool + .spawner() + .spawn(self.staging_belt.recall()) + .expect("Recall staging belt"); + + self.local_pool.run_until_stalled(); + + mouse_interaction + } +} diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs deleted file mode 100644 index 72e58a50..00000000 --- a/wgpu/src/window/swap_chain.rs +++ /dev/null @@ -1,61 +0,0 @@ -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, - ) -> Result<(wgpu::SwapChainOutput, &Viewport), wgpu::TimeOut> { - let viewport = &self.viewport; - - self.raw.get_next_texture().map(|output| (output, 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::Mailbox, - }, - ) -} diff --git a/winit/Cargo.toml b/winit/Cargo.toml index b6662451..39a6a5fa 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_winit" -version = "0.1.1" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A winit runtime for Iced" @@ -11,16 +11,25 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -debug = [] +debug = ["iced_native/debug"] [dependencies] -winit = "0.22" +winit = "0.24" window_clipboard = "0.1" log = "0.4" +thiserror = "1.0" [dependencies.iced_native] -version = "0.2" +version = "0.3" path = "../native" +[dependencies.iced_graphics] +version = "0.1" +path = "../graphics" + +[dependencies.iced_futures] +version = "0.2" +path = "../futures" + [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" diff --git a/winit/README.md b/winit/README.md index 34dec1b3..721baa14 100644 --- a/winit/README.md +++ b/winit/README.md @@ -8,7 +8,9 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop. - +<p align="center"> + <img alt="The native target" src="../docs/graphs/native.png" width="80%"> +</p> [documentation]: https://docs.rs/iced_winit [`iced_native`]: ../native @@ -18,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t Add `iced_winit` as a dependency in your `Cargo.toml`: ```toml -iced_winit = "0.1" +iced_winit = "0.2" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/winit/src/application.rs b/winit/src/application.rs index f6bc8fcc..d1a94864 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,39 +1,36 @@ +//! Create interactive, native cross-platform applications. +mod state; + +pub use state::State; + +use crate::conversion; +use crate::mouse; use crate::{ - conversion, mouse, size::Size, window, Cache, Clipboard, Command, Debug, - Element, Executor, Mode, Proxy, Runtime, Settings, Subscription, - UserInterface, + Clipboard, Color, Command, Debug, Error, Executor, Mode, Proxy, Runtime, + Settings, Size, Subscription, }; +use iced_futures::futures; +use iced_futures::futures::channel::mpsc; +use iced_graphics::window; +use iced_native::program::Program; +use iced_native::{Cache, UserInterface}; + +use std::mem::ManuallyDrop; + /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run /// your GUI application by simply calling [`run`](#method.run). It will run in /// its own window. /// -/// An [`Application`](trait.Application.html) can execute asynchronous actions -/// by returning a [`Command`](struct.Command.html) in some of its methods. +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Sized { - /// The graphics backend to use to draw the [`Application`]. - /// - /// [`Application`]: trait.Application.html - 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. - /// - /// [`Application`]: trait.Application.html - type Message: std::fmt::Debug + Send; - +pub trait Application: Program { /// The data needed to initialize your [`Application`]. - /// - /// [`Application`]: trait.Application.html type Flags; /// Initializes the [`Application`] with the flags provided to @@ -41,36 +38,17 @@ pub trait Application: Sized { /// /// Here is where you should return the initial state of your app. /// - /// Additionally, you can return a [`Command`](struct.Command.html) if you - /// need to perform some async action in the background on startup. This is - /// useful if you want to load state from a file, perform an initial HTTP - /// request, etc. - /// - /// [`Application`]: trait.Application.html - /// [`run`]: #method.run.html - /// [`Settings`]: struct.Settings.html + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); /// Returns the current title of the [`Application`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - /// - /// [`Application`]: trait.Application.html fn title(&self) -> String; - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the background. - /// - /// [`Application`]: trait.Application.html - /// [`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. /// @@ -78,16 +56,11 @@ pub trait Application: Sized { /// [`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::Backend as window::Backend>::Renderer>; + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } /// Returns the current [`Application`] mode. /// @@ -95,244 +68,255 @@ pub trait Application: Sized { /// is returned. /// /// By default, an application will run in windowed mode. - /// - /// [`Application`]: trait.Application.html fn mode(&self) -> Mode { Mode::Windowed } - /// Runs the [`Application`] with the provided [`Settings`]. + /// Returns the background [`Color`] of the [`Application`]. /// - /// On native platforms, this method will take control of the current thread - /// and __will NOT return__. + /// By default, it returns [`Color::WHITE`]. + fn background_color(&self) -> Color { + Color::WHITE + } + + /// Returns the scale factor of the [`Application`]. /// - /// It should probably be that last thing you call in your `main` function. + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). /// - /// [`Application`]: trait.Application.html - /// [`Settings`]: struct.Settings.html - fn run( - settings: Settings<Self::Flags>, - backend_settings: <Self::Backend as window::Backend>::Settings, - ) where - Self: 'static, - { - use window::Backend as _; - use winit::{ - event::{self, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }; - - let mut debug = Debug::new(); - - debug.startup_started(); - let event_loop = EventLoop::with_user_event(); - let mut external_messages = Vec::new(); - - let mut runtime = { - let executor = Self::Executor::new().expect("Create executor"); - - Runtime::new(executor, Proxy::new(event_loop.create_proxy())) - }; + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self) -> f64 { + 1.0 + } +} - let flags = settings.flags; - let (mut application, init_command) = - runtime.enter(|| Self::new(flags)); - runtime.spawn(init_command); +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor<Renderer = A::Renderer> + 'static, +{ + use futures::task; + use futures::Future; + use winit::event_loop::EventLoop; - let subscription = application.subscription(); - runtime.track(subscription); + let mut debug = Debug::new(); + debug.startup_started(); - let mut title = application.title(); - let mut mode = application.mode(); + let (compositor, renderer) = C::new(compositor_settings)?; - let window = { - let mut window_builder = WindowBuilder::new(); + let event_loop = EventLoop::with_user_event(); - let (width, height) = settings.window.size; + let mut runtime = { + let proxy = Proxy::new(event_loop.create_proxy()); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - 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, - )); + Runtime::new(executor, proxy) + }; - #[cfg(target_os = "windows")] - { - use winit::platform::windows::WindowBuilderExtWindows; + let (application, init_command) = { + let flags = settings.flags; - if let Some(parent) = settings.window.platform_specific.parent { - window_builder = window_builder.with_parent_window(parent); - } - } + runtime.enter(|| A::new(flags)) + }; - window_builder.build(&event_loop).expect("Open window") - }; + let subscription = application.subscription(); - let mut size = Size::new(window.inner_size(), window.scale_factor()); - let mut resized = false; + runtime.spawn(init_command); + runtime.track(subscription); - let clipboard = Clipboard::new(&window); - let (mut backend, mut renderer) = Self::Backend::new(backend_settings); + let window = settings + .window + .into_builder( + &application.title(), + application.mode(), + event_loop.primary_monitor(), + ) + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; - let surface = backend.create_surface(&window); + let (mut sender, receiver) = mpsc::unbounded(); - let mut swap_chain = { - let physical_size = size.physical(); + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + renderer, + window, + runtime, + debug, + receiver, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); - backend.create_swap_chain( - &surface, - physical_size.width, - physical_size.height, - ) - }; + event_loop.run(move |event, _, control_flow| { + use winit::event_loop::ControlFlow; - let user_interface = build_user_interface( - &mut application, - Cache::default(), - &mut renderer, - size.logical(), - &mut debug, - ); + if let ControlFlow::Exit = control_flow { + return; + } - debug.draw_started(); - let mut primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); + if let Some(event) = event.to_static() { + sender.start_send(event).expect("Send event"); - let mut cache = Some(user_interface.into_cache()); - let mut events = Vec::new(); - let mut mouse_interaction = mouse::Interaction::default(); - let mut modifiers = winit::event::ModifiersState::default(); - debug.startup_finished(); + let poll = instance.as_mut().poll(&mut context); - window.request_redraw(); + *control_flow = match poll { + task::Poll::Pending => ControlFlow::Wait, + task::Poll::Ready(_) => ControlFlow::Exit, + }; + } + }); +} - event_loop.run(move |event, _, control_flow| match event { +async fn run_instance<A, E, C>( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + window: winit::window::Window, + mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver<winit::event::Event<'_, A::Message>>, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor<Renderer = A::Renderer> + 'static, +{ + use iced_futures::futures::stream::StreamExt; + use winit::event; + + let surface = compositor.create_surface(&window); + let clipboard = Clipboard::new(&window); + + let mut state = State::new(&application, &window); + let mut viewport_version = state.viewport_version(); + let mut swap_chain = { + let physical_size = state.physical_size(); + + compositor.create_swap_chain( + &surface, + physical_size.width, + physical_size.height, + ) + }; + + let mut user_interface = ManuallyDrop::new(build_user_interface( + &mut application, + Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + )); + + let mut primitive = + user_interface.draw(&mut renderer, state.cursor_position()); + let mut mouse_interaction = mouse::Interaction::default(); + + let mut events = Vec::new(); + let mut messages = Vec::new(); + + debug.startup_finished(); + + while let Some(event) = receiver.next().await { + match event { event::Event::MainEventsCleared => { - if events.is_empty() && external_messages.is_empty() { - return; + if events.is_empty() && messages.is_empty() { + continue; } - // 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. - let mut user_interface = build_user_interface( - &mut application, - cache.take().unwrap(), + debug.event_processing_started(); + + let statuses = user_interface.update( + &events, + state.cursor_position(), + clipboard.as_ref().map(|c| c as _), &mut renderer, - size.logical(), - &mut debug, + &mut messages, ); - debug.event_processing_started(); - 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(); - if messages.is_empty() { - debug.draw_started(); - primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); - - cache = Some(user_interface.into_cache()); - } else { - // When there are messages, we are forced to rebuild twice - // for now :^) - let temp_cache = user_interface.into_cache(); - - for message in messages { - log::debug!("Updating"); - - debug.log_message(&message); - - debug.update_started(); - 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(); - - if title != new_title { - window.set_title(&new_title); - - title = new_title; - } - - // Update window mode - let new_mode = application.mode(); - - if mode != new_mode { - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - new_mode, - )); + for event in events.drain(..).zip(statuses.into_iter()) { + runtime.broadcast(event); + } - mode = new_mode; - } + if !messages.is_empty() { + let cache = + ManuallyDrop::into_inner(user_interface).into_cache(); - let user_interface = build_user_interface( + // Update application + update( &mut application, - temp_cache, - &mut renderer, - size.logical(), + &mut runtime, &mut debug, + &mut messages, ); - debug.draw_started(); - primitive = user_interface.draw(&mut renderer); - debug.draw_finished(); + // Update window + state.synchronize(&application, &window); - cache = Some(user_interface.into_cache()); + user_interface = ManuallyDrop::new(build_user_interface( + &mut application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); } + debug.draw_started(); + primitive = + user_interface.draw(&mut renderer, state.cursor_position()); + debug.draw_finished(); + window.request_redraw(); } event::Event::UserEvent(message) => { - external_messages.push(message); + messages.push(message); } event::Event::RedrawRequested(_) => { debug.render_started(); + let current_viewport_version = state.viewport_version(); - if resized { - let physical_size = size.physical(); + if viewport_version != current_viewport_version { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); - swap_chain = backend.create_swap_chain( + debug.layout_started(); + user_interface = ManuallyDrop::new( + ManuallyDrop::into_inner(user_interface) + .relayout(logical_size, &mut renderer), + ); + debug.layout_finished(); + + debug.draw_started(); + primitive = user_interface + .draw(&mut renderer, state.cursor_position()); + debug.draw_finished(); + + swap_chain = compositor.create_swap_chain( &surface, physical_size.width, physical_size.height, ); - resized = false; + viewport_version = current_viewport_version; } - let new_mouse_interaction = backend.draw( + let new_mouse_interaction = compositor.draw( &mut renderer, &mut swap_chain, + state.viewport(), + state.background_color(), &primitive, - size.scale_factor(), &debug.overlay(), ); @@ -353,81 +337,90 @@ pub trait Application: Sized { event: window_event, .. } => { - match window_event { - WindowEvent::Resized(new_size) => { - size = Size::new(new_size, window.scale_factor()); - resized = true; - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - #[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; - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: - Some(winit::event::VirtualKeyCode::F12), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => debug.toggle(), - _ => {} + if requests_exit(&window_event, state.modifiers()) { + break; } + state.update(&window, &window_event, &mut debug); + if let Some(event) = conversion::window_event( &window_event, - size.scale_factor(), - modifiers, + state.scale_factor(), + state.modifiers(), ) { events.push(event); } } - _ => { - *control_flow = ControlFlow::Wait; - } - }) + _ => {} + } } + + // Manually drop the user interface + drop(ManuallyDrop::into_inner(user_interface)); } -fn build_user_interface<'a, A: Application>( +/// Returns true if the provided event should cause an [`Application`] to +/// exit. +pub fn requests_exit( + event: &winit::event::WindowEvent<'_>, + _modifiers: winit::event::ModifiersState, +) -> bool { + use winit::event::WindowEvent; + + match event { + WindowEvent::CloseRequested => true, + #[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() => true, + _ => false, + } +} + +/// Builds a [`UserInterface`] for the provided [`Application`], logging +/// [`struct@Debug`] information accordingly. +pub 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>, + renderer: &mut A::Renderer, + size: Size, debug: &mut Debug, -) -> UserInterface<'a, A::Message, <A::Backend as window::Backend>::Renderer> { +) -> UserInterface<'a, A::Message, A::Renderer> { debug.view_started(); let view = application.view(); debug.view_finished(); 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, - ); + let user_interface = UserInterface::build(view, size, cache, renderer); debug.layout_finished(); user_interface } + +/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`]. +pub fn update<A: Application, E: Executor>( + application: &mut A, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + debug: &mut Debug, + messages: &mut Vec<A::Message>, +) { + for message in messages.drain(..) { + debug.log_message(&message); + + debug.update_started(); + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + runtime.spawn(command); + } + + let subscription = application.subscription(); + runtime.track(subscription); +} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs new file mode 100644 index 00000000..46297370 --- /dev/null +++ b/winit/src/application/state.rs @@ -0,0 +1,205 @@ +use crate::conversion; +use crate::{Application, Color, Debug, Mode, Point, Size, Viewport}; + +use std::marker::PhantomData; +use winit::event::{Touch, WindowEvent}; +use winit::window::Window; + +/// The state of a windowed [`Application`]. +#[derive(Debug, Clone)] +pub struct State<A: Application> { + title: String, + mode: Mode, + background_color: Color, + scale_factor: f64, + viewport: Viewport, + viewport_version: usize, + cursor_position: winit::dpi::PhysicalPosition<f64>, + modifiers: winit::event::ModifiersState, + application: PhantomData<A>, +} + +impl<A: Application> State<A> { + /// Creates a new [`State`] for the provided [`Application`] and window. + pub fn new(application: &A, window: &Window) -> Self { + let title = application.title(); + let mode = application.mode(); + let background_color = application.background_color(); + let scale_factor = application.scale_factor(); + + let viewport = { + let physical_size = window.inner_size(); + + Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor() * scale_factor, + ) + }; + + Self { + title, + mode, + background_color, + scale_factor, + viewport, + viewport_version: 0, + // TODO: Encode cursor availability in the type-system + cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), + modifiers: winit::event::ModifiersState::default(), + application: PhantomData, + } + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.background_color + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// Returns the version of the [`Viewport`] of the [`State`]. + /// + /// The version is incremented every time the [`Viewport`] changes. + pub fn viewport_version(&self) -> usize { + self.viewport_version + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size<u32> { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size<f32> { + self.viewport.logical_size() + } + + /// Returns the current scale factor of the [`Viewport`] of the [`State`]. + pub fn scale_factor(&self) -> f64 { + self.viewport.scale_factor() + } + + /// Returns the current cursor position of the [`State`]. + pub fn cursor_position(&self) -> Point { + conversion::cursor_position( + self.cursor_position, + self.viewport.scale_factor(), + ) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> winit::event::ModifiersState { + self.modifiers + } + + /// Processes the provided window event and updates the [`State`] + /// accordingly. + pub fn update( + &mut self, + window: &Window, + event: &WindowEvent<'_>, + _debug: &mut Debug, + ) { + match event { + WindowEvent::Resized(new_size) => { + let size = Size::new(new_size.width, new_size.height); + + self.viewport = Viewport::with_physical_size( + size, + window.scale_factor() * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + new_inner_size, + } => { + let size = + Size::new(new_inner_size.width, new_inner_size.height); + + self.viewport = Viewport::with_physical_size( + size, + new_scale_factor * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::CursorMoved { position, .. } + | WindowEvent::Touch(Touch { + location: position, .. + }) => { + self.cursor_position = *position; + } + WindowEvent::CursorLeft { .. } => { + // TODO: Encode cursor availability in the type-system + self.cursor_position = + winit::dpi::PhysicalPosition::new(-1.0, -1.0); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + self.modifiers = *new_modifiers; + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } + } + + /// Synchronizes the [`State`] with its [`Application`] and its respective + /// window. + /// + /// Normally an [`Application`] should be synchronized with its [`State`] + /// and window after calling [`Application::update`]. + /// + /// [`Application::update`]: crate::Program::update + pub fn synchronize(&mut self, application: &A, window: &Window) { + // Update window title + let new_title = application.title(); + + if self.title != new_title { + window.set_title(&new_title); + + self.title = new_title; + } + + // Update window mode + let new_mode = application.mode(); + + if self.mode != new_mode { + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + new_mode, + )); + + self.mode = new_mode; + } + + // Update background color + self.background_color = application.background_color(); + + // Update scale factor + let new_scale_factor = application.scale_factor(); + + if self.scale_factor != new_scale_factor { + let size = window.inner_size(); + + self.viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor() * new_scale_factor, + ); + + self.scale_factor = new_scale_factor; + } + } +} diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 1ff029ab..93d53b11 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -5,8 +5,6 @@ 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() } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b887db6e..f073c474 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -2,10 +2,11 @@ //! //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -use crate::{ - keyboard::{self, KeyCode, ModifiersState}, - mouse, window, Event, Mode, -}; +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; +use crate::{Event, Mode, Point}; /// Converts a winit window event into an iced event. pub fn window_event( @@ -24,14 +25,27 @@ pub fn window_event( height: logical_size.height, })) } + WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + let logical_size = new_inner_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, + position: Point::new(position.x as f32, position.y as f32), })) } + WindowEvent::CursorEntered { .. } => { + Some(Event::Mouse(mouse::Event::CursorEntered)) + } + WindowEvent::CursorLeft { .. } => { + Some(Event::Mouse(mouse::Event::CursorLeft)) + } WindowEvent::MouseInput { button, state, .. } => { let button = mouse_button(*button); @@ -75,7 +89,7 @@ pub fn window_event( .. } => Some(Event::Keyboard({ let key_code = key_code(*virtual_keycode); - let modifiers = modifiers_state(modifiers); + let modifiers = self::modifiers(modifiers); match state { winit::event::ElementState::Pressed => { @@ -92,6 +106,9 @@ pub fn window_event( } } })), + WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( + keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), + )), WindowEvent::HoveredFile(path) => { Some(Event::Window(window::Event::FileHovered(path.clone()))) } @@ -101,16 +118,18 @@ pub fn window_event( WindowEvent::HoveredFileCancelled => { Some(Event::Window(window::Event::FilesHoveredLeft)) } + WindowEvent::Touch(touch) => { + Some(Event::Touch(touch_event(*touch, scale_factor))) + } _ => None, } } /// Converts a [`Mode`] to a [`winit`] fullscreen mode. /// -/// [`Mode`]: ../enum.Mode.html /// [`winit`]: https://github.com/rust-windowing/winit pub fn fullscreen( - monitor: winit::monitor::MonitorHandle, + monitor: Option<winit::monitor::MonitorHandle>, mode: Mode, ) -> Option<winit::window::Fullscreen> { match mode { @@ -154,7 +173,9 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Right => mouse::Button::Right, winit::event::MouseButton::Middle => mouse::Button::Middle, - winit::event::MouseButton::Other(other) => mouse::Button::Other(other), + winit::event::MouseButton::Other(other) => { + mouse::Button::Other(other as u8) + } } } @@ -163,10 +184,10 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -pub fn modifiers_state( +pub fn modifiers( modifiers: winit::event::ModifiersState, -) -> ModifiersState { - ModifiersState { +) -> keyboard::Modifiers { + keyboard::Modifiers { shift: modifiers.shift(), control: modifiers.ctrl(), alt: modifiers.alt(), @@ -174,11 +195,56 @@ pub fn modifiers_state( } } +/// Converts a physical cursor position to a logical `Point`. +pub fn cursor_position( + position: winit::dpi::PhysicalPosition<f64>, + scale_factor: f64, +) -> Point { + let logical_position = position.to_logical(scale_factor); + + Point::new(logical_position.x, logical_position.y) +} + +/// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +pub fn touch_event( + touch: winit::event::Touch, + scale_factor: f64, +) -> touch::Event { + let id = touch::Finger(touch.id); + let position = { + let location = touch.location.to_logical::<f64>(scale_factor); + + Point::new(location.x as f32, location.y as f32) + }; + + match touch.phase { + winit::event::TouchPhase::Started => { + touch::Event::FingerPressed { id, position } + } + winit::event::TouchPhase::Moved => { + touch::Event::FingerMoved { id, position } + } + winit::event::TouchPhase::Ended => { + touch::Event::FingerLifted { id, position } + } + winit::event::TouchPhase::Cancelled => { + touch::Event::FingerLost { id, position } + } + } +} + /// 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 -pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { +pub fn key_code( + virtual_keycode: winit::event::VirtualKeyCode, +) -> keyboard::KeyCode { + use keyboard::KeyCode; + match virtual_keycode { winit::event::VirtualKeyCode::Key1 => KeyCode::Key1, winit::event::VirtualKeyCode::Key2 => KeyCode::Key2, @@ -272,7 +338,8 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::Numpad9 => KeyCode::Numpad9, winit::event::VirtualKeyCode::AbntC1 => KeyCode::AbntC1, winit::event::VirtualKeyCode::AbntC2 => KeyCode::AbntC2, - winit::event::VirtualKeyCode::Add => KeyCode::Add, + winit::event::VirtualKeyCode::NumpadAdd => KeyCode::NumpadAdd, + winit::event::VirtualKeyCode::Plus => KeyCode::Plus, winit::event::VirtualKeyCode::Apostrophe => KeyCode::Apostrophe, winit::event::VirtualKeyCode::Apps => KeyCode::Apps, winit::event::VirtualKeyCode::At => KeyCode::At, @@ -283,8 +350,8 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::Colon => KeyCode::Colon, winit::event::VirtualKeyCode::Comma => KeyCode::Comma, winit::event::VirtualKeyCode::Convert => KeyCode::Convert, - winit::event::VirtualKeyCode::Decimal => KeyCode::Decimal, - winit::event::VirtualKeyCode::Divide => KeyCode::Divide, + winit::event::VirtualKeyCode::NumpadDecimal => KeyCode::NumpadDecimal, + winit::event::VirtualKeyCode::NumpadDivide => KeyCode::NumpadDivide, winit::event::VirtualKeyCode::Equals => KeyCode::Equals, winit::event::VirtualKeyCode::Grave => KeyCode::Grave, winit::event::VirtualKeyCode::Kana => KeyCode::Kana, @@ -298,7 +365,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::MediaSelect => KeyCode::MediaSelect, winit::event::VirtualKeyCode::MediaStop => KeyCode::MediaStop, winit::event::VirtualKeyCode::Minus => KeyCode::Minus, - winit::event::VirtualKeyCode::Multiply => KeyCode::Multiply, + winit::event::VirtualKeyCode::NumpadMultiply => KeyCode::NumpadMultiply, winit::event::VirtualKeyCode::Mute => KeyCode::Mute, winit::event::VirtualKeyCode::MyComputer => KeyCode::MyComputer, winit::event::VirtualKeyCode::NavigateForward => { @@ -326,7 +393,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::Slash => KeyCode::Slash, winit::event::VirtualKeyCode::Sleep => KeyCode::Sleep, winit::event::VirtualKeyCode::Stop => KeyCode::Stop, - winit::event::VirtualKeyCode::Subtract => KeyCode::Subtract, + winit::event::VirtualKeyCode::NumpadSubtract => KeyCode::NumpadSubtract, winit::event::VirtualKeyCode::Sysrq => KeyCode::Sysrq, winit::event::VirtualKeyCode::Tab => KeyCode::Tab, winit::event::VirtualKeyCode::Underline => KeyCode::Underline, @@ -345,6 +412,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode { winit::event::VirtualKeyCode::Copy => KeyCode::Copy, winit::event::VirtualKeyCode::Paste => KeyCode::Paste, winit::event::VirtualKeyCode::Cut => KeyCode::Cut, + winit::event::VirtualKeyCode::Asterisk => KeyCode::Asterisk, } } diff --git a/winit/src/error.rs b/winit/src/error.rs new file mode 100644 index 00000000..8e1d20e8 --- /dev/null +++ b/winit/src/error.rs @@ -0,0 +1,27 @@ +use iced_futures::futures; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The futures executor could not be created. + #[error("the futures executor could not be created")] + ExecutorCreationFailed(futures::io::Error), + + /// The application window could not be created. + #[error("the application window could not be created")] + WindowCreationFailed(winit::error::OsError), + + /// A suitable graphics adapter or device could not be found. + #[error("a suitable graphics adapter or device could not be found")] + GraphicsAdapterNotFound, +} + +impl From<iced_graphics::Error> for Error { + fn from(error: iced_graphics::Error) -> Error { + match error { + iced_graphics::Error::AdapterNotFound => { + Error::GraphicsAdapterNotFound + } + } + } +} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index f99e1290..c9f324dd 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1,6 +1,6 @@ //! A windowing shell for Iced, on top of [`winit`]. //! -//!  +//!  //! //! `iced_winit` offers some convenient abstractions on top of [`iced_native`] //! to quickstart development when using [`winit`]. @@ -13,8 +13,7 @@ //! //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native //! [`winit`]: https://github.com/rust-windowing/winit -//! [`Application`]: trait.Application.html -//! [`conversion`]: conversion +//! [`conversion`]: crate::conversion #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] @@ -25,28 +24,20 @@ pub use iced_native::*; pub use winit; +pub mod application; pub mod conversion; pub mod settings; -mod application; mod clipboard; +mod error; mod mode; mod proxy; -mod size; - -// We disable debug capabilities on release builds unless the `debug` feature -// is explicitly enabled. -#[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 error::Error; pub use mode::Mode; +pub use proxy::Proxy; pub use settings::Settings; -use debug::Debug; -use proxy::Proxy; +pub use iced_graphics::Viewport; diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index cff6ca72..7b9074d7 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -5,6 +5,8 @@ use iced_native::futures::{ }; use std::pin::Pin; +/// An event loop proxy that implements `Sink`. +#[derive(Debug)] pub struct Proxy<Message: 'static> { raw: winit::event_loop::EventLoopProxy<Message>, } @@ -18,6 +20,7 @@ impl<Message: 'static> Clone for Proxy<Message> { } impl<Message: 'static> Proxy<Message> { + /// Creates a new [`Proxy`] from an `EventLoopProxy`. pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self { Self { raw } } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index d58c51f0..2e8715cd 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -8,42 +8,110 @@ mod platform; pub use platform::PlatformSpecific; +use crate::conversion; +use crate::Mode; +use winit::monitor::MonitorHandle; +use winit::window::WindowBuilder; + /// The settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[derive(Debug, Clone, Default)] pub struct Settings<Flags> { /// The [`Window`] settings - /// - /// [`Window`]: struct.Window.html pub window: Window, /// The data needed to initialize an [`Application`]. /// - /// [`Application`]: trait.Application.html + /// [`Application`]: crate::Application pub flags: Flags, } /// The window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct Window { /// The size of the window. pub size: (u32, u32), + /// The minimum size of the window. + pub min_size: Option<(u32, u32)>, + + /// The maximum size of the window. + pub max_size: Option<(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, + /// Whether the window should be transparent. + pub transparent: bool, + + /// Whether the window will always be on top of other windows. + pub always_on_top: bool, + + /// The window icon, which is also usually used in the taskbar + pub icon: Option<winit::window::Icon>, + /// Platform specific settings. pub platform_specific: platform::PlatformSpecific, } +impl Window { + /// Converts the window settings into a `WindowBuilder` from `winit`. + pub fn into_builder( + self, + title: &str, + mode: Mode, + primary_monitor: Option<MonitorHandle>, + ) -> WindowBuilder { + let mut window_builder = WindowBuilder::new(); + + let (width, height) = self.size; + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { width, height }) + .with_resizable(self.resizable) + .with_decorations(self.decorations) + .with_transparent(self.transparent) + .with_window_icon(self.icon) + .with_always_on_top(self.always_on_top) + .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); + + if let Some((width, height)) = self.min_size { + window_builder = window_builder + .with_min_inner_size(winit::dpi::LogicalSize { width, height }); + } + + if let Some((width, height)) = self.max_size { + window_builder = window_builder + .with_max_inner_size(winit::dpi::LogicalSize { width, height }); + } + + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + + if let Some(parent) = self.platform_specific.parent { + window_builder = window_builder.with_parent_window(parent); + } + } + + window_builder + } +} + impl Default for Window { fn default() -> Window { Window { size: (1024, 768), + min_size: None, + max_size: None, resizable: true, decorations: true, + transparent: false, + always_on_top: false, + icon: None, platform_specific: Default::default(), } } diff --git a/winit/src/size.rs b/winit/src/size.rs deleted file mode 100644 index 7e3056d4..00000000 --- a/winit/src/size.rs +++ /dev/null @@ -1,30 +0,0 @@ -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 - } -} |