diff options
181 files changed, 5042 insertions, 1366 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4a3aad7c..8a891884 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: hecrj ko_fi: hecrj_ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b0bf033..a00e2a22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,11 @@ jobs: - name: Install cargo-deb run: cargo install cargo-deb - uses: actions/checkout@master + - name: Install dependencies + run: | + export DEBIAN_FRONTED=noninteractive + sudo apt-get -qq update + sudo apt-get install -y libxkbcommon-dev - name: Enable Link Time Optimizations run: | echo "[profile.release]" >> Cargo.toml @@ -67,6 +72,8 @@ jobs: env: MACOSX_DEPLOYMENT_TARGET: 10.14 run: cargo build --verbose --release --package todos + - name: Open binary via double-click + run: chmod +x target/release/todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc531abf..0450f13d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,12 @@ jobs: with: rust-version: ${{ matrix.rust }} - uses: actions/checkout@master + - name: Install dependencies + if: matrix.os == 'ubuntu-latest' + run: | + export DEBIAN_FRONTED=noninteractive + sudo apt-get -qq update + sudo apt-get install -y libxkbcommon-dev - name: Run tests run: | cargo test --verbose --all diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cb739d..3feaeba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,70 @@ 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] + +## [0.3.0] - 2021-03-31 ### Added -- Support for the [`smol`] async runtime. [#699] +- Touch support. [#57] [#650] (thanks to @simlay and @discordance!) +- Clipboard write access for + - `TextInput` widget. [#770] + - `Application::update`. [#773] +- `image::Viewer` widget. It allows panning and scaling of an image. [#319] (thanks to @tarkah!) +- `Tooltip` widget. It annotates content with some text on mouse hover. [#465] (thanks to @yusdacra!) +- Support for the [`smol`] async runtime. [#699] (thanks to @JayceFayne!) +- Support for graceful exiting when using the `Application` trait. [#804] +- Image format features in [`iced_wgpu`] to reduce code bloat. [#392] (thanks to @unrelentingtech!) +- `Focused` and `Unfocused` variant to `window::Event`. [#701] (thanks to @cossonleo!) +- `WGPU_BACKEND` environment variable to configure the internal graphics backend of `iced_wgpu`. [#789] (thanks to @Cupnfish!) + +### Changed +- The `TitleBar` of a `PaneGrid` now supports generic elements. [#657] (thanks to @clarkmoody!) +- The `Error` type now implements `Send` and `Sync`. [#719] (thanks to @taiki-e!) +- The `Style` types in `iced_style` now implement `Clone` and `Copy`. [#720] (thanks to @taiki-e!) +- The following dependencies have been updated: + - [`font-kit`] → `0.10` [#669] + - [`glutin`] → `0.26` [#658] + - [`resvg`] → `0.12` [#669] + - [`tokio`] → `1.0` [#672] (thanks to @yusdacra!) + - [`winit`] → `0.24` [#658] + - [`wgpu`] → `0.7` [#725] (thanks to @PolyMeilex) +- The following examples were improved: + - `download_progress` now showcases multiple file downloads at once. [#283] (thanks to @Folyd!) + - `solar_system` uses the new `rand` API. [#760] (thanks to @TriedAngle!) +### Fixed +- Button events not being propagated to contents. [#668] +- Incorrect overlay implementation for the `Button` widget. [#764] +- `Viewport::physical_width` returning the wrong value. [#700] +- Outdated documentation for the `Sandbox` trait. [#710] + +[#57]: https://github.com/hecrj/iced/pull/57 +[#283]: https://github.com/hecrj/iced/pull/283 +[#319]: https://github.com/hecrj/iced/pull/319 +[#392]: https://github.com/hecrj/iced/pull/392 +[#465]: https://github.com/hecrj/iced/pull/465 +[#650]: https://github.com/hecrj/iced/pull/650 +[#657]: https://github.com/hecrj/iced/pull/657 +[#658]: https://github.com/hecrj/iced/pull/658 +[#668]: https://github.com/hecrj/iced/pull/668 +[#669]: https://github.com/hecrj/iced/pull/669 +[#672]: https://github.com/hecrj/iced/pull/672 [#699]: https://github.com/hecrj/iced/pull/699 +[#700]: https://github.com/hecrj/iced/pull/700 +[#701]: https://github.com/hecrj/iced/pull/701 +[#710]: https://github.com/hecrj/iced/pull/710 +[#719]: https://github.com/hecrj/iced/pull/719 +[#720]: https://github.com/hecrj/iced/pull/720 +[#725]: https://github.com/hecrj/iced/pull/725 +[#760]: https://github.com/hecrj/iced/pull/760 +[#764]: https://github.com/hecrj/iced/pull/764 +[#770]: https://github.com/hecrj/iced/pull/770 +[#773]: https://github.com/hecrj/iced/pull/773 +[#789]: https://github.com/hecrj/iced/pull/789 +[#804]: https://github.com/hecrj/iced/pull/804 [`smol`]: https://github.com/smol-rs/smol +[`winit`]: https://github.com/rust-windowing/winit +[`glutin`]: https://github.com/rust-windowing/glutin +[`font-kit`]: https://github.com/servo/font-kit ## [0.2.0] - 2020-11-26 ### Added @@ -148,7 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [`wasm-bindgen-futures`]: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures [`resvg`]: https://github.com/RazrFalcon/resvg [`raqote`]: https://github.com/jrmuizel/raqote -[`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.1/wgpu +[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu ## [0.1.0-beta] - 2019-11-25 @@ -160,7 +219,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - First release! :tada: -[Unreleased]: https://github.com/hecrj/iced/compare/0.2.0...HEAD +[Unreleased]: https://github.com/hecrj/iced/compare/0.3.0...HEAD +[0.3.0]: https://github.com/hecrj/iced/compare/0.2.0...0.3.0 [0.2.0]: https://github.com/hecrj/iced/compare/0.1.1...0.2.0 [0.1.1]: https://github.com/hecrj/iced/compare/0.1.0...0.1.1 [0.1.0]: https://github.com/hecrj/iced/compare/0.1.0-beta...0.1.0 @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A cross-platform GUI library inspired by Elm" @@ -10,6 +10,7 @@ documentation = "https://docs.rs/iced" readme = "README.md" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] +resolver = "2" [features] default = ["wgpu", "default_system_font"] @@ -70,7 +71,9 @@ members = [ "examples/events", "examples/game_of_life", "examples/geometry", - "examples/integration", + "examples/integration_opengl", + "examples/integration_wgpu", + "examples/menu", "examples/pane_grid", "examples/pick_list", "examples/pokedex", @@ -82,24 +85,25 @@ members = [ "examples/styling", "examples/svg", "examples/todos", - "examples/tour", "examples/tooltip", + "examples/tour", + "examples/url_handler", ] [dependencies] -iced_core = { version = "0.3", path = "core" } -iced_futures = { version = "0.2", path = "futures" } +iced_core = { version = "0.4", path = "core" } +iced_futures = { version = "0.3", path = "futures" } thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -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} +iced_winit = { version = "0.3", path = "winit" } +iced_glutin = { version = "0.2", path = "glutin", optional = true } +iced_wgpu = { version = "0.4", path = "wgpu", optional = true } +iced_glow = { version = "0.2", path = "glow", optional = true} [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_web = { version = "0.3", path = "web" } +iced_web = { version = "0.4", path = "web" } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] -features = ["image", "svg", "canvas"] +features = ["image", "svg", "canvas", "qr_code"] @@ -1,4 +1,9 @@ -# Iced +<div align="center"> + +<img src="docs/logo.svg" width="140px" /> + +# iced + [](https://github.com/hecrj/iced/actions) [][documentation] [](https://crates.io/crates/iced) @@ -8,13 +13,13 @@ A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by [Elm]. -<div align="center"> - <a href="https://gfycat.com/littlesanehalicore"> - <img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="350px"> - </a> - <a href="https://gfycat.com/politeadorableiberianmole"> - <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> - </a> +<a href="https://gfycat.com/littlesanehalicore"> + <img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="350px"> +</a> +<a href="https://gfycat.com/politeadorableiberianmole"> + <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> +</a> + </div> ## Features @@ -32,7 +37,7 @@ Inspired by [Elm]. * A [windowing shell] * A [web runtime] leveraging the DOM -__Iced is currently experimental software.__ [Take a look at the roadmap], +__iced is currently experimental software.__ [Take a look at the roadmap], [check out the issues], and [feel free to contribute!] [Cross-platform support]: https://github.com/hecrj/iced/blob/master/docs/images/todos_desktop.jpg?raw=true @@ -55,16 +60,16 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.2" +iced = "0.3" ``` -__Iced moves fast and the `master` branch can contain breaking changes!__ If +__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 ## Overview -Inspired by [The Elm Architecture], Iced expects you to split user interfaces +Inspired by [The Elm Architecture], iced expects you to split user interfaces into four different concepts: * __State__ — the state of your application @@ -154,7 +159,7 @@ impl Counter { } ``` -And that's everything! We just wrote a whole user interface. Iced is now able +And that's everything! We just wrote a whole user interface. iced is now able to: 1. Take the result of our __view logic__ and layout its widgets. @@ -165,7 +170,7 @@ to: Browse the [documentation] and the [examples] to learn more! ## Implementation details -Iced was originally born as an attempt at bringing the simplicity of [Elm] and +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 2019 in [this pull request]. @@ -178,7 +183,7 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular: <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%"> + <img alt="iced ecosystem" src="docs/graphs/ecosystem.png" width="80%"> </a> </p> @@ -199,7 +204,7 @@ awesome folks) over the `#games-and-graphics` and `#gui-and-ui` channels in the [Rust Community Discord]. I go by `lone_scientist#9554` there. ## Sponsors -The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] +The development of iced is sponsored by the [Cryptowatch] team at [Kraken.com] [documentation]: https://docs.rs/iced/ [examples]: https://github.com/hecrj/iced/tree/master/examples diff --git a/core/Cargo.toml b/core/Cargo.toml index a859c868..54d927af 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "The essential concepts of Iced" @@ -8,6 +8,7 @@ license = "MIT" repository = "https://github.com/hecrj/iced" [dependencies] +bitflags = "1.2" [dependencies.palette] version = "0.5.0" diff --git a/core/README.md b/core/README.md index 3ec053ac..86d631d2 100644 --- a/core/README.md +++ b/core/README.md @@ -18,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.3" +iced_core = "0.4" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index 61e017ad..6827a4db 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,8 +1,10 @@ -//! Reuse basic keyboard types. +//! Listen to keyboard events. mod event; +mod hotkey; mod key_code; mod modifiers; pub use event::Event; +pub use hotkey::Hotkey; pub use key_code::KeyCode; pub use modifiers::Modifiers; diff --git a/core/src/keyboard/hotkey.rs b/core/src/keyboard/hotkey.rs new file mode 100644 index 00000000..310ef286 --- /dev/null +++ b/core/src/keyboard/hotkey.rs @@ -0,0 +1,18 @@ +use crate::keyboard::{KeyCode, Modifiers}; + +/// Representation of a hotkey, consists on the combination of a [`KeyCode`] and [`Modifiers`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Hotkey { + /// The key that represents this hotkey. + pub key: KeyCode, + + /// The list of modifiers that represents this hotkey. + pub modifiers: Modifiers, +} + +impl Hotkey { + /// Creates a new [`Hotkey`] with the given [`Modifiers`] and [`KeyCode`]. + pub fn new(modifiers: Modifiers, key: KeyCode) -> Self { + Self { modifiers, key } + } +} diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index d2a0500e..e61f145a 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -1,20 +1,65 @@ -/// 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, +use bitflags::bitflags; - /// 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, +bitflags! { + /// The current state of the keyboard modifiers. + #[derive(Default)] + pub struct Modifiers: u32{ + /// The "shift" key. + const SHIFT = 0b100 << 0; + // const LSHIFT = 0b010 << 0; + // const RSHIFT = 0b001 << 0; + // + /// The "control" key. + const CTRL = 0b100 << 3; + // const LCTRL = 0b010 << 3; + // const RCTRL = 0b001 << 3; + // + /// The "alt" key. + const ALT = 0b100 << 6; + // const LALT = 0b010 << 6; + // const RALT = 0b001 << 6; + // + /// The "windows" key on Windows, "command" key on Mac, and + /// "super" key on Linux. + const LOGO = 0b100 << 9; + // const LLOGO = 0b010 << 9; + // const RLOGO = 0b001 << 9; + } } impl Modifiers { + /// The "command" key. + /// + /// This is normally the main modifier to be used for hotkeys. + /// + /// On macOS, this is equivalent to `Self::LOGO`. + /// Ohterwise, this is equivalent to `Self::CTRL`. + pub const COMMAND: Self = if cfg!(target_os = "macos") { + Self::LOGO + } else { + Self::CTRL + }; + + /// Returns true if the [`SHIFT`] key is pressed in the [`Modifiers`]. + pub fn shift(self) -> bool { + self.contains(Self::SHIFT) + } + + /// Returns true if the [`CTRL`] key is pressed in the [`Modifiers`]. + pub fn control(self) -> bool { + self.contains(Self::CTRL) + } + + /// Returns true if the [`ALT`] key is pressed in the [`Modifiers`]. + pub fn alt(self) -> bool { + self.contains(Self::ALT) + } + + /// Returns true if the [`LOGO`] key is pressed in the [`Modifiers`]. + pub fn logo(self) -> bool { + self.contains(Self::LOGO) + } + /// 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 @@ -22,24 +67,13 @@ impl Modifiers { /// /// - It is the `logo` or command key (⌘) on macOS /// - It is the `control` key on other platforms - pub fn is_command_pressed(self) -> bool { + pub fn command(self) -> bool { #[cfg(target_os = "macos")] - let is_pressed = self.logo; + let is_pressed = self.logo(); #[cfg(not(target_os = "macos"))] - let is_pressed = self.control; + 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/lib.rs b/core/src/lib.rs index f2d21a5f..a0decdab 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -15,13 +15,16 @@ #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] pub mod keyboard; +pub mod menu; pub mod mouse; +pub mod text; mod align; mod background; mod color; mod font; mod length; +mod padding; mod point; mod rectangle; mod size; @@ -32,6 +35,8 @@ pub use background::Background; pub use color::Color; pub use font::Font; pub use length::Length; +pub use menu::Menu; +pub use padding::Padding; pub use point::Point; pub use rectangle::Rectangle; pub use size::Size; diff --git a/core/src/menu.rs b/core/src/menu.rs new file mode 100644 index 00000000..8a679085 --- /dev/null +++ b/core/src/menu.rs @@ -0,0 +1,145 @@ +//! Build menus for your application. +use crate::keyboard::Hotkey; + +/// Menu representation. +/// +/// This can be used by `shell` implementations to create a menu. +#[derive(Debug, Clone)] +pub struct Menu<Message> { + entries: Vec<Entry<Message>>, +} + +impl<Message> PartialEq for Menu<Message> { + fn eq(&self, other: &Self) -> bool { + self.entries == other.entries + } +} + +impl<Message> Menu<Message> { + /// Creates an empty [`Menu`]. + pub fn new() -> Self { + Self::with_entries(Vec::new()) + } + + /// Creates a new [`Menu`] with the given entries. + pub fn with_entries(entries: Vec<Entry<Message>>) -> Self { + Self { entries } + } + + /// Returns a [`MenuEntry`] iterator. + pub fn iter(&self) -> impl Iterator<Item = &Entry<Message>> { + self.entries.iter() + } + + /// Adds an [`Entry`] to the [`Menu`]. + pub fn push(mut self, entry: Entry<Message>) -> Self { + self.entries.push(entry); + self + } + + /// Maps the `Message` of the [`Menu`] using the provided function. + /// + /// This is useful to compose menus and split them into different + /// abstraction levels. + pub fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Menu<B> { + // TODO: Use a boxed trait to avoid reallocation of entries + Menu { + entries: self + .entries + .into_iter() + .map(|entry| entry.map(f)) + .collect(), + } + } +} + +/// Represents one of the possible entries used to build a [`Menu`]. +#[derive(Debug, Clone)] +pub enum Entry<Message> { + /// Item for a [`Menu`] + Item { + /// The title of the item + title: String, + /// The [`Hotkey`] to activate the item, if any + hotkey: Option<Hotkey>, + /// The message generated when the item is activated + on_activation: Message, + }, + /// Dropdown for a [`Menu`] + Dropdown { + /// Title of the dropdown + title: String, + /// The submenu of the dropdown + submenu: Menu<Message>, + }, + /// Separator for a [`Menu`] + Separator, +} + +impl<Message> Entry<Message> { + /// Creates an [`Entry::Item`]. + pub fn item<S: Into<String>>( + title: S, + hotkey: impl Into<Option<Hotkey>>, + on_activation: Message, + ) -> Self { + let title = title.into(); + let hotkey = hotkey.into(); + + Self::Item { + title, + hotkey, + on_activation, + } + } + + /// Creates an [`Entry::Dropdown`]. + pub fn dropdown<S: Into<String>>(title: S, submenu: Menu<Message>) -> Self { + let title = title.into(); + + Self::Dropdown { title, submenu } + } + + fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Entry<B> { + match self { + Self::Item { + title, + hotkey, + on_activation, + } => Entry::Item { + title, + hotkey, + on_activation: f(on_activation), + }, + Self::Dropdown { title, submenu } => Entry::Dropdown { + title, + submenu: submenu.map(f), + }, + Self::Separator => Entry::Separator, + } + } +} + +impl<Message> PartialEq for Entry<Message> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Entry::Item { title, hotkey, .. }, + Entry::Item { + title: other_title, + hotkey: other_hotkey, + .. + }, + ) => title == other_title && hotkey == other_hotkey, + ( + Entry::Dropdown { title, submenu }, + Entry::Dropdown { + title: other_title, + submenu: other_submenu, + }, + ) => title == other_title && submenu == other_submenu, + (Entry::Separator, Entry::Separator) => true, + _ => false, + } + } +} diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 25ce6ac3..48214f65 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -1,4 +1,4 @@ -//! Reuse basic mouse types. +//! Handle mouse events. mod button; mod event; mod interaction; diff --git a/core/src/padding.rs b/core/src/padding.rs new file mode 100644 index 00000000..22467d6b --- /dev/null +++ b/core/src/padding.rs @@ -0,0 +1,107 @@ +/// An amount of space to pad for each side of a box +/// +/// You can leverage the `From` trait to build [`Padding`] conveniently: +/// +/// ``` +/// # use iced_core::Padding; +/// # +/// let padding = Padding::from(20); // 20px on all sides +/// let padding = Padding::from([10, 20]); // top/bottom, left/right +/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left +/// ``` +/// +/// Normally, the `padding` method of a widget will ask for an `Into<Padding>`, +/// so you can easily write: +/// +/// ``` +/// # use iced_core::Padding; +/// # +/// # struct Widget; +/// # +/// impl Widget { +/// # pub fn new() -> Self { Self } +/// # +/// pub fn padding(mut self, padding: impl Into<Padding>) -> Self { +/// // ... +/// self +/// } +/// } +/// +/// let widget = Widget::new().padding(20); // 20px on all sides +/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right +/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left +/// ``` +#[derive(Debug, Hash, Copy, Clone)] +pub struct Padding { + /// Top padding + pub top: u16, + /// Right padding + pub right: u16, + /// Bottom padding + pub bottom: u16, + /// Left padding + pub left: u16, +} + +impl Padding { + /// Padding of zero + pub const ZERO: Padding = Padding { + top: 0, + right: 0, + bottom: 0, + left: 0, + }; + + /// Create a Padding that is equal on all sides + pub const fn new(padding: u16) -> Padding { + Padding { + top: padding, + right: padding, + bottom: padding, + left: padding, + } + } + + /// Returns the total amount of vertical [`Padding`]. + pub fn vertical(self) -> u16 { + self.top + self.bottom + } + + /// Returns the total amount of horizontal [`Padding`]. + pub fn horizontal(self) -> u16 { + self.left + self.right + } +} + +impl std::convert::From<u16> for Padding { + fn from(p: u16) -> Self { + Padding { + top: p, + right: p, + bottom: p, + left: p, + } + } +} + +impl std::convert::From<[u16; 2]> for Padding { + fn from(p: [u16; 2]) -> Self { + Padding { + top: p[0], + right: p[1], + bottom: p[0], + left: p[1], + } + } +} + +impl std::convert::From<[u16; 4]> for Padding { + fn from(p: [u16; 4]) -> Self { + Padding { + top: p[0], + right: p[1], + bottom: p[2], + left: p[3], + } + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 0a7f5fe2..4e082051 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -105,8 +105,8 @@ impl Rectangle<f32> { Rectangle { x: self.x as u32, y: self.y as u32, - width: self.width.ceil() as u32, - height: self.height.ceil() as u32, + width: self.width as u32, + height: self.height as u32, } } } diff --git a/core/src/size.rs b/core/src/size.rs index 9ea9e686..6745c6c8 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,4 +1,4 @@ -use crate::Vector; +use crate::{Padding, Vector}; use std::f32; /// An amount of space in 2 dimensions. @@ -28,10 +28,10 @@ impl Size { pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); /// Increments the [`Size`] to account for the given padding. - pub fn pad(&self, padding: f32) -> Self { + pub fn pad(&self, padding: Padding) -> Self { Size { - width: self.width + padding * 2.0, - height: self.height + padding * 2.0, + width: self.width + padding.horizontal() as f32, + height: self.height + padding.vertical() as f32, } } } diff --git a/core/src/text.rs b/core/src/text.rs new file mode 100644 index 00000000..ded22eef --- /dev/null +++ b/core/src/text.rs @@ -0,0 +1,29 @@ +//! Draw and interact with text. +use crate::Vector; + +/// The result of hit testing on text. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Hit { + /// The point was within the bounds of the returned character index. + CharOffset(usize), + /// The provided point was not within the bounds of a glyph. The index + /// of the character with the closest centeroid position is returned, + /// as well as its delta. + NearestCharOffset(usize, Vector), +} + +impl Hit { + /// Computes the cursor position corresponding to this [`HitTestResult`] . + pub fn cursor(&self) -> usize { + match self { + Self::CharOffset(i) => *i, + Self::NearestCharOffset(i, delta) => { + if delta.x > f32::EPSILON { + i + 1 + } else { + *i + } + } + } + } +} diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 00000000..387db449 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,14 @@ +<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="68" height="68" rx="18" fill="url(#paint0_linear)"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M52.2952 16.986L43.0273 26.4088L41.0081 24.4228L50.2761 15L52.2952 16.986ZM43.6175 17.0578L29.7156 31.192L27.6964 29.206L41.5983 15.0718L43.6175 17.0578ZM23.3109 24.9347C25.065 23.1615 26.8188 21.3887 30.4157 17.7317L32.4348 19.7177C28.8353 23.3774 27.0795 25.1523 25.3241 26.9267C23.8744 28.3921 22.425 29.8572 19.9355 32.3857L30.4685 35.1388L39.0223 26.442L41.0414 28.428L32.4876 37.1247L35.4218 47.6353L49.3019 33.7718L51.3033 35.7756L35.1269 51.9327L20.0283 47.7728L15.6165 32.7371L16.2 32.1438C19.7995 28.4842 21.5555 26.7091 23.3109 24.9347ZM32.6721 48.3186L29.7581 37.8804L19.2731 35.1398L22.3017 45.4614L32.6721 48.3186ZM48.3953 29.7021L50.7414 27.3561L48.7387 25.3535L46.3844 27.7078L36.4462 37.812L38.4654 39.7979L48.3953 29.7021Z" fill="url(#paint1_linear)"/> +<defs> +<linearGradient id="paint0_linear" x1="34" y1="0" x2="34" y2="68" gradientUnits="userSpaceOnUse"> +<stop stop-color="#00A3FF"/> +<stop offset="1" stop-color="#3300FF"/> +</linearGradient> +<linearGradient id="paint1_linear" x1="20.5" y1="47.4656" x2="60" y2="6.96558" gradientUnits="userSpaceOnUse"> +<stop stop-color="white"/> +<stop offset="1" stop-color="white" stop-opacity="0.65"/> +</linearGradient> +</defs> +</svg>
\ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 10c28cf5..1b9f4b56 100644 --- a/examples/README.md +++ b/examples/README.md @@ -98,7 +98,8 @@ A bunch of simpler examples exist: - [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress. - [`events`](events), a log of native events displayed using a conditional `Subscription`. - [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu). -- [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application. +- [`integration_opengl`](integration_opengl), a demonstration of how to integrate Iced in an existing OpenGL application. +- [`integration_wgpu`](integration_wgpu), a demonstration of how to integrate Iced in an existing [`wgpu`] 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]. @@ -116,6 +117,7 @@ cargo run --package <example> [`lyon`]: https://github.com/nical/lyon [PokéAPI]: https://pokeapi.co/ [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg +[`wgpu`]: https://github.com/gfx-rs/wgpu ## [Coffee] Since [Iced was born in May 2019], it has been powering the user interfaces in diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 18e6364b..446c190b 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,22 +1,30 @@ use iced::{ - executor, Align, Application, Checkbox, Clipboard, Column, Command, - Container, Element, Length, Settings, Subscription, Text, + button, executor, Align, Application, Button, Checkbox, Clipboard, Column, + Command, Container, Element, HorizontalAlignment, Length, Settings, + Subscription, Text, }; +use iced_native::{window, Event}; pub fn main() -> iced::Result { - Events::run(Settings::default()) + Events::run(Settings { + exit_on_close_request: false, + ..Settings::default() + }) } #[derive(Debug, Default)] struct Events { last: Vec<iced_native::Event>, enabled: bool, + exit: button::State, + should_exit: bool, } #[derive(Debug, Clone)] enum Message { EventOccurred(iced_native::Event), Toggled(bool), + Exit, } impl Application for Events { @@ -38,27 +46,35 @@ impl Application for Events { _clipboard: &mut Clipboard, ) -> Command<Message> { match message { - Message::EventOccurred(event) => { + Message::EventOccurred(event) if self.enabled => { self.last.push(event); if self.last.len() > 5 { let _ = self.last.remove(0); } } + Message::EventOccurred(event) => { + if let Event::Window(window::Event::CloseRequested) = event { + self.should_exit = true; + } + } Message::Toggled(enabled) => { self.enabled = enabled; } + Message::Exit => { + self.should_exit = true; + } }; Command::none() } fn subscription(&self) -> Subscription<Message> { - if self.enabled { - iced_native::subscription::events().map(Message::EventOccurred) - } else { - Subscription::none() - } + iced_native::subscription::events().map(Message::EventOccurred) + } + + fn should_exit(&self) -> bool { + self.should_exit } fn view(&mut self) -> Element<Message> { @@ -75,11 +91,22 @@ impl Application for Events { Message::Toggled, ); + let exit = Button::new( + &mut self.exit, + Text::new("Exit") + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .width(Length::Units(100)) + .padding(10) + .on_press(Message::Exit); + let content = Column::new() .align_items(Align::Center) .spacing(20) .push(events) - .push(toggle); + .push(toggle) + .push(exit); Container::new(content) .width(Length::Fill) diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 64599163..c3e16e8b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -6,9 +6,11 @@ mod style; use grid::Grid; use iced::button::{self, Button}; use iced::executor; +use iced::menu::{self, Menu}; use iced::pick_list::{self, PickList}; use iced::slider::{self, Slider}; use iced::time; +use iced::window; use iced::{ Align, Application, Checkbox, Clipboard, Column, Command, Container, Element, Length, Row, Settings, Subscription, Text, @@ -19,6 +21,10 @@ use std::time::{Duration, Instant}; pub fn main() -> iced::Result { GameOfLife::run(Settings { antialiasing: true, + window: window::Settings { + position: window::Position::Centered, + ..window::Settings::default() + }, ..Settings::default() }) } @@ -128,6 +134,13 @@ impl Application for GameOfLife { } } + fn menu(&self) -> Menu<Message> { + Menu::with_entries(vec![menu::Entry::dropdown( + "Presets", + Preset::menu().map(Message::PresetPicked), + )]) + } + fn view(&mut self) -> Element<Message> { let version = self.version; let selected_speed = self.next_speed.unwrap_or(self.speed); diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs index 05157b6a..1c199a72 100644 --- a/examples/game_of_life/src/preset.rs +++ b/examples/game_of_life/src/preset.rs @@ -1,3 +1,5 @@ +use iced::menu::{self, Menu}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Preset { Custom, @@ -26,6 +28,17 @@ pub static ALL: &[Preset] = &[ ]; impl Preset { + pub fn menu() -> Menu<Self> { + Menu::with_entries( + ALL.iter() + .copied() + .map(|preset| { + menu::Entry::item(preset.to_string(), None, preset) + }) + .collect(), + ) + } + pub fn life(self) -> Vec<(isize, isize)> { #[rustfmt::skip] let cells = match self { diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs index 6605826f..be9a0e96 100644 --- a/examples/game_of_life/src/style.rs +++ b/examples/game_of_life/src/style.rs @@ -171,6 +171,7 @@ impl pick_list::StyleSheet for PickList { }, border_radius: 2.0, icon_size: 0.5, + ..pick_list::Style::default() } } diff --git a/examples/integration_opengl/Cargo.toml b/examples/integration_opengl/Cargo.toml new file mode 100644 index 00000000..0917f2ec --- /dev/null +++ b/examples/integration_opengl/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "integration_opengl" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced_glutin = { path = "../../glutin" } +iced_glow = { path = "../../glow" } +iced_winit = { path = "../../winit" } +env_logger = "0.8" +glow = "0.6" diff --git a/examples/integration_opengl/README.md b/examples/integration_opengl/README.md new file mode 100644 index 00000000..08cce7a6 --- /dev/null +++ b/examples/integration_opengl/README.md @@ -0,0 +1,16 @@ +## OpenGL integration + +A demonstration of how to integrate Iced in an existing graphical OpenGL application. + +The __[`main`]__ file contains all the code of the example. + +<div align="center"> + <a href="https://imgbox.com/9P9ETcod" target="_blank"><img src="https://images2.imgbox.com/2a/51/9P9ETcod_o.gif" alt="image host"/></a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package integration_gl +``` + +[`main`]: src/main.rs diff --git a/examples/integration_opengl/src/controls.rs b/examples/integration_opengl/src/controls.rs new file mode 100644 index 00000000..13b7fbc2 --- /dev/null +++ b/examples/integration_opengl/src/controls.rs @@ -0,0 +1,110 @@ +use iced_glow::Renderer; +use iced_glutin::{ + slider, Align, Clipboard, Color, Column, Command, Element, Length, Program, + Row, Slider, Text, +}; + +pub struct Controls { + background_color: Color, + sliders: [slider::State; 3], +} + +#[derive(Debug, Clone)] +pub enum Message { + BackgroundColorChanged(Color), +} + +impl Controls { + pub fn new() -> Controls { + Controls { + background_color: Color::BLACK, + sliders: Default::default(), + } + } + + pub fn background_color(&self) -> Color { + self.background_color + } +} + +impl Program for Controls { + type Renderer = Renderer; + type Message = Message; + type Clipboard = Clipboard; + + fn update( + &mut self, + message: Message, + _clipboard: &mut Clipboard, + ) -> Command<Message> { + match message { + Message::BackgroundColorChanged(color) => { + self.background_color = color; + } + } + + Command::none() + } + + fn view(&mut self) -> Element<Message, Renderer> { + let [r, g, b] = &mut self.sliders; + let background_color = self.background_color; + + let sliders = Row::new() + .width(Length::Units(500)) + .spacing(20) + .push( + Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color + }) + }) + .step(0.01), + ) + .push( + Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color + }) + }) + .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) + .height(Length::Fill) + .align_items(Align::End) + .push( + Column::new() + .width(Length::Fill) + .align_items(Align::End) + .push( + Column::new() + .padding(10) + .spacing(10) + .push( + Text::new("Background color") + .color(Color::WHITE), + ) + .push(sliders) + .push( + Text::new(format!("{:?}", background_color)) + .size(14) + .color(Color::WHITE), + ), + ), + ) + .into() + } +} diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs new file mode 100644 index 00000000..f80915d2 --- /dev/null +++ b/examples/integration_opengl/src/main.rs @@ -0,0 +1,181 @@ +mod controls; +mod scene; + +use controls::Controls; +use scene::Scene; + +use glow; +use glow::*; +use iced_glow::{Backend, Renderer, Settings, Viewport}; +use iced_glutin::glutin; +use iced_glutin::glutin::event::{Event, WindowEvent}; +use iced_glutin::glutin::event_loop::ControlFlow; +use iced_glutin::{program, Clipboard, Debug, Size}; +use iced_winit::conversion; +use iced_winit::winit; +use winit::{dpi::PhysicalPosition, event::ModifiersState}; + +pub fn main() { + env_logger::init(); + let (gl, event_loop, windowed_context, shader_version) = { + let el = glutin::event_loop::EventLoop::new(); + + let wb = glutin::window::WindowBuilder::new() + .with_title("OpenGL integration example") + .with_inner_size(glutin::dpi::LogicalSize::new(1024.0, 768.0)); + + let windowed_context = glutin::ContextBuilder::new() + .with_vsync(true) + .build_windowed(wb, &el) + .unwrap(); + + unsafe { + let windowed_context = windowed_context.make_current().unwrap(); + + let gl = glow::Context::from_loader_function(|s| { + windowed_context.get_proc_address(s) as *const _ + }); + + // 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); + + (gl, el, windowed_context, "#version 410") + } + }; + + let physical_size = windowed_context.window().inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + windowed_context.window().scale_factor(), + ); + + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); + let mut modifiers = ModifiersState::default(); + let mut clipboard = Clipboard::connect(&windowed_context.window()); + + let mut renderer = Renderer::new(Backend::new(&gl, Settings::default())); + + let mut debug = Debug::new(); + + let controls = Controls::new(); + let mut state = program::State::new( + controls, + viewport.logical_size(), + conversion::cursor_position(cursor_position, viewport.scale_factor()), + &mut renderer, + &mut debug, + ); + let mut resized = false; + + event_loop.run(move |event, _, control_flow| { + let scene = Scene::new(&gl, &shader_version); + *control_flow = ControlFlow::Wait; + + match event { + Event::LoopDestroyed => return, + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + modifiers = new_modifiers; + } + WindowEvent::Resized(physical_size) => { + viewport = Viewport::with_physical_size( + Size::new( + physical_size.width, + physical_size.height, + ), + windowed_context.window().scale_factor(), + ); + + resized = true; + } + WindowEvent::CloseRequested => { + scene.cleanup(&gl); + *control_flow = ControlFlow::Exit + } + _ => (), + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + &event, + windowed_context.window().scale_factor(), + modifiers, + ) { + state.queue_event(event); + } + } + Event::MainEventsCleared => { + // 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(), + ), + &mut renderer, + &mut clipboard, + &mut debug, + ); + + // and request a redraw + windowed_context.window().request_redraw(); + } + } + Event::RedrawRequested(_) => { + if resized { + let size = windowed_context.window().inner_size(); + + unsafe { + gl.viewport( + 0, + 0, + size.width as i32, + size.height as i32, + ); + } + + resized = false; + } + + let program = state.program(); + { + // We clear the frame + scene.clear(&gl, program.background_color()); + + // Draw the scene + scene.draw(&gl); + } + + // And then iced on top + let mouse_interaction = renderer.backend_mut().draw( + &gl, + &viewport, + state.primitive(), + &debug.overlay(), + ); + // Update the mouse cursor + windowed_context.window().set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); + + windowed_context.swap_buffers().unwrap(); + } + _ => (), + } + }); +} diff --git a/examples/integration_opengl/src/scene.rs b/examples/integration_opengl/src/scene.rs new file mode 100644 index 00000000..ccca0d29 --- /dev/null +++ b/examples/integration_opengl/src/scene.rs @@ -0,0 +1,99 @@ +use glow::*; +use iced_glow::Color; + +pub struct Scene { + program: glow::Program, + vertex_array: glow::VertexArray, +} + +impl Scene { + pub fn new(gl: &glow::Context, shader_version: &str) -> Self { + unsafe { + let vertex_array = gl + .create_vertex_array() + .expect("Cannot create vertex array"); + gl.bind_vertex_array(Some(vertex_array)); + + let program = gl.create_program().expect("Cannot create program"); + + let (vertex_shader_source, fragment_shader_source) = ( + r#"const vec2 verts[3] = vec2[3]( + vec2(0.5f, 1.0f), + vec2(0.0f, 0.0f), + vec2(1.0f, 0.0f) + ); + out vec2 vert; + void main() { + vert = verts[gl_VertexID]; + gl_Position = vec4(vert - 0.5, 0.0, 1.0); + }"#, + r#"precision highp float; + in vec2 vert; + out vec4 color; + void main() { + color = vec4(vert, 0.5, 1.0); + }"#, + ); + + let shader_sources = [ + (glow::VERTEX_SHADER, vertex_shader_source), + (glow::FRAGMENT_SHADER, fragment_shader_source), + ]; + + 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, + &format!("{}\n{}", shader_version, 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); + } + + gl.use_program(Some(program)); + Self { + program, + vertex_array, + } + } + } + + pub fn clear(&self, gl: &glow::Context, background_color: Color) { + let [r, g, b, a] = background_color.into_linear(); + unsafe { + gl.clear_color(r, g, b, a); + gl.clear(glow::COLOR_BUFFER_BIT); + } + } + + pub fn draw(&self, gl: &glow::Context) { + unsafe { + gl.draw_arrays(glow::TRIANGLES, 0, 3); + } + } + + pub fn cleanup(&self, gl: &glow::Context) { + unsafe { + gl.delete_program(self.program); + gl.delete_vertex_array(self.vertex_array); + } + } +} diff --git a/examples/integration/Cargo.toml b/examples/integration_wgpu/Cargo.toml index 4515502f..a088dd1b 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration_wgpu/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "integration" +name = "integration_wgpu" version = "0.1.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" @@ -7,5 +7,5 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } -iced_wgpu = { path = "../../wgpu" } +iced_wgpu = { path = "../../wgpu", features = ["spirv"] } env_logger = "0.8" diff --git a/examples/integration/README.md b/examples/integration_wgpu/README.md index d5aabc19..c51c2c65 100644 --- a/examples/integration/README.md +++ b/examples/integration_wgpu/README.md @@ -1,6 +1,6 @@ -## Integration +## `wgpu` integration -A demonstration of how to integrate Iced in an existing graphical application. +A demonstration of how to integrate Iced in an existing [`wgpu`] application. The __[`main`]__ file contains all the code of the example. @@ -16,3 +16,4 @@ cargo run --package integration ``` [`main`]: src/main.rs +[`wgpu`]: https://github.com/gfx-rs/wgpu diff --git a/examples/integration/src/controls.rs b/examples/integration_wgpu/src/controls.rs index 36ee9b7e..36ee9b7e 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration_wgpu/src/controls.rs diff --git a/examples/integration/src/main.rs b/examples/integration_wgpu/src/main.rs index b57c73d8..7ef148bc 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration_wgpu/src/main.rs @@ -31,10 +31,10 @@ pub fn main() { let mut clipboard = Clipboard::connect(&window); // Initialize wgpu - let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); let surface = unsafe { instance.create_surface(&window) }; - let (mut device, queue) = futures::executor::block_on(async { + let (format, (mut device, queue)) = futures::executor::block_on(async { let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, @@ -43,29 +43,32 @@ pub fn main() { .await .expect("Request adapter"); - adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - features: wgpu::Features::empty(), - limits: wgpu::Limits::default(), - }, - None, - ) - .await - .expect("Request device") + ( + surface + .get_preferred_format(&adapter) + .expect("Get preferred format"), + adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + }, + None, + ) + .await + .expect("Request device"), + ) }); - let format = wgpu::TextureFormat::Bgra8UnormSrgb; - - let mut swap_chain = { + { let size = window.inner_size(); - device.create_swap_chain( - &surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::RENDER_ATTACHMENT, - format: format, + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, width: size.width, height: size.height, present_mode: wgpu::PresentMode::Mailbox, @@ -85,7 +88,7 @@ pub fn main() { // Initialize iced let mut debug = Debug::new(); let mut renderer = - Renderer::new(Backend::new(&mut device, Settings::default())); + Renderer::new(Backend::new(&mut device, Settings::default(), format)); let mut state = program::State::new( controls, @@ -155,10 +158,10 @@ pub fn main() { if resized { let size = window.inner_size(); - swap_chain = device.create_swap_chain( - &surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::RENDER_ATTACHMENT, + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: format, width: size.width, height: size.height, @@ -169,55 +172,68 @@ pub fn main() { resized = false; } - let frame = swap_chain.get_current_frame().expect("Next frame"); + match surface.get_current_frame() { + Ok(frame) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + + let program = state.program(); + + let view = frame.output.texture.create_view(&wgpu::TextureViewDescriptor::default()); + + { + // We clear the frame + let mut render_pass = scene.clear( + &view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } + + // And then iced on top + let mouse_interaction = renderer.backend_mut().draw( + &mut device, + &mut staging_belt, + &mut encoder, + &view, + &viewport, + state.primitive(), + &debug.overlay(), + ); - let mut encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); + // Then we submit the work + staging_belt.finish(); + queue.submit(Some(encoder.finish())); - let program = state.program(); + // Update the mouse cursor + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); - { - // We clear the frame - let mut render_pass = scene.clear( - &frame.output.view, - &mut encoder, - program.background_color(), - ); + // And recall staging buffers + local_pool + .spawner() + .spawn(staging_belt.recall()) + .expect("Recall staging buffers"); - // Draw the scene - scene.draw(&mut render_pass); + local_pool.run_until_stalled(); + } + Err(error) => match error { + wgpu::SurfaceError::OutOfMemory => { + panic!("Swapchain error: {}. Rendering cannot continue.", error) + } + _ => { + // Try rendering again next frame. + window.request_redraw(); + } + }, } - - // And then iced on top - let mouse_interaction = renderer.backend_mut().draw( - &mut device, - &mut staging_belt, - &mut encoder, - &frame.output.view, - &viewport, - state.primitive(), - &debug.overlay(), - ); - - // Then we submit the work - staging_belt.finish(); - queue.submit(Some(encoder.finish())); - - // Update the mouse cursor - window.set_cursor_icon( - iced_winit::conversion::mouse_interaction( - mouse_interaction, - ), - ); - - // And recall staging buffers - local_pool - .spawner() - .spawn(staging_belt.recall()) - .expect("Recall staging buffers"); - - local_pool.run_until_stalled(); } _ => {} } diff --git a/examples/integration/src/scene.rs b/examples/integration_wgpu/src/scene.rs index 36c0a41d..0b2b1fcd 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration_wgpu/src/scene.rs @@ -20,8 +20,8 @@ impl Scene { ) -> wgpu::RenderPass<'a> { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, - color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, + color_attachments: &[wgpu::RenderPassColorAttachment { + view: target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear({ @@ -75,15 +75,16 @@ fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline { entry_point: "main", targets: &[wgpu::ColorTargetState { format: wgpu::TextureFormat::Bgra8UnormSrgb, - color_blend: wgpu::BlendState::REPLACE, - alpha_blend: wgpu::BlendState::REPLACE, - write_mask: wgpu::ColorWrite::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, }], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Ccw, - cull_mode: wgpu::CullMode::None, ..Default::default() }, depth_stencil: None, diff --git a/examples/integration/src/shader/frag.spv b/examples/integration_wgpu/src/shader/frag.spv Binary files differindex 9d6807c9..9d6807c9 100644 --- a/examples/integration/src/shader/frag.spv +++ b/examples/integration_wgpu/src/shader/frag.spv diff --git a/examples/integration/src/shader/vert.spv b/examples/integration_wgpu/src/shader/vert.spv Binary files differindex 0cabc9c0..0cabc9c0 100644 --- a/examples/integration/src/shader/vert.spv +++ b/examples/integration_wgpu/src/shader/vert.spv diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml new file mode 100644 index 00000000..44597734 --- /dev/null +++ b/examples/menu/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "menu" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" }
\ No newline at end of file diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs new file mode 100644 index 00000000..7403713c --- /dev/null +++ b/examples/menu/src/main.rs @@ -0,0 +1,117 @@ +use iced::menu::{self, Menu}; +use iced::{ + executor, Application, Clipboard, Command, Container, Element, Length, + Settings, Text, +}; +use iced_native::keyboard::{Hotkey, KeyCode, Modifiers}; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +#[derive(Debug, Default)] +struct App { + selected: Option<Entry>, +} + +#[derive(Debug, Clone)] +enum Entry { + One, + Two, + Three, + A, + B, + C, +} + +#[derive(Debug, Clone)] +enum Message { + MenuActivated(Entry), +} + +impl Application for App { + type Executor = executor::Default; + type Message = Message; + type Flags = (); + + fn new(_flags: ()) -> (App, Command<Message>) { + (App::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Menu - Iced") + } + + fn menu(&self) -> Menu<Message> { + let alt = Modifiers::ALT; + let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT; + + Menu::with_entries(vec![ + menu::Entry::dropdown( + "First", + Menu::with_entries(vec![ + menu::Entry::item( + "One", + Hotkey::new(alt, KeyCode::F1), + Message::MenuActivated(Entry::One), + ), + menu::Entry::item( + "Two", + Hotkey::new(alt, KeyCode::F2), + Message::MenuActivated(Entry::Two), + ), + menu::Entry::Separator, + menu::Entry::item( + "Three", + Hotkey::new(alt, KeyCode::F3), + Message::MenuActivated(Entry::Three), + ), + ]), + ), + menu::Entry::dropdown( + "Second", + Menu::with_entries(vec![ + menu::Entry::item( + "A", + Hotkey::new(ctrl_shift, KeyCode::A), + Message::MenuActivated(Entry::A), + ), + menu::Entry::item( + "B", + Hotkey::new(ctrl_shift, KeyCode::B), + Message::MenuActivated(Entry::B), + ), + menu::Entry::Separator, + menu::Entry::item( + "C", + Hotkey::new(ctrl_shift, KeyCode::C), + Message::MenuActivated(Entry::C), + ), + ]), + ), + ]) + } + + fn update( + &mut self, + message: Message, + _clipboard: &mut Clipboard, + ) -> Command<Message> { + match message { + Message::MenuActivated(entry) => self.selected = Some(entry), + } + + Command::none() + } + + fn view(&mut self) -> Element<Message> { + Container::new( + Text::new(format!("Selected {:?}", self.selected)).size(48), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 4b87a568..3bd8aa25 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -11,7 +11,7 @@ pub fn main() -> iced::Result { } struct Example { - panes: pane_grid::State<Content>, + panes: pane_grid::State<Pane>, panes_created: usize, focus: Option<pane_grid::Pane>, } @@ -24,6 +24,7 @@ enum Message { Clicked(pane_grid::Pane), Dragged(pane_grid::DragEvent), Resized(pane_grid::ResizeEvent), + TogglePin(pane_grid::Pane), Close(pane_grid::Pane), CloseFocused, } @@ -34,7 +35,7 @@ impl Application for Example { type Flags = (); fn new(_flags: ()) -> (Self, Command<Message>) { - let (panes, _) = pane_grid::State::new(Content::new(0)); + let (panes, _) = pane_grid::State::new(Pane::new(0)); ( Example { @@ -60,7 +61,7 @@ impl Application for Example { let result = self.panes.split( axis, &pane, - Content::new(self.panes_created), + Pane::new(self.panes_created), ); if let Some((pane, _)) = result { @@ -74,7 +75,7 @@ impl Application for Example { let result = self.panes.split( axis, &pane, - Content::new(self.panes_created), + Pane::new(self.panes_created), ); if let Some((pane, _)) = result { @@ -106,6 +107,12 @@ impl Application for Example { self.panes.swap(&pane, &target); } Message::Dragged(_) => {} + Message::TogglePin(pane) => { + if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane) + { + *is_pinned = !*is_pinned; + } + } Message::Close(pane) => { if let Some((_, sibling)) = self.panes.close(&pane) { self.focus = Some(sibling); @@ -113,8 +120,14 @@ impl Application for Example { } Message::CloseFocused => { if let Some(pane) = self.focus { - if let Some((_, sibling)) = self.panes.close(&pane) { - self.focus = Some(sibling); + if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = self.panes.close(&pane) + { + self.focus = Some(sibling); + } + } } } } @@ -133,7 +146,7 @@ impl Application for Example { Event::Keyboard(keyboard::Event::KeyPressed { modifiers, key_code, - }) if modifiers.is_command_pressed() => handle_hotkey(key_code), + }) if modifiers.command() => handle_hotkey(key_code), _ => None, } }) @@ -143,12 +156,20 @@ impl Application for Example { let focus = self.focus; let total_panes = self.panes.len(); - let pane_grid = PaneGrid::new(&mut self.panes, |pane, content| { - let is_focused = focus == Some(pane); + let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| { + let is_focused = focus == Some(id); + + let text = if pane.is_pinned { "Unpin" } else { "Pin" }; + let pin_button = + Button::new(&mut pane.pin_button, Text::new(text).size(14)) + .on_press(Message::TogglePin(id)) + .style(style::Button::Pin) + .padding(3); let title = Row::with_children(vec![ + pin_button.into(), Text::new("Pane").into(), - Text::new(content.id.to_string()) + Text::new(pane.content.id.to_string()) .color(if is_focused { PANE_ID_COLOR_FOCUSED } else { @@ -159,12 +180,17 @@ impl Application for Example { .spacing(5); let title_bar = pane_grid::TitleBar::new(title) + .controls(pane.controls.view(id, total_panes, pane.is_pinned)) .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 }) + pane_grid::Content::new(pane.content.view( + id, + total_panes, + pane.is_pinned, + )) + .title_bar(title_bar) + .style(style::Pane { is_focused }) }) .width(Length::Fill) .height(Length::Fill) @@ -212,6 +238,13 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { } } +struct Pane { + pub is_pinned: bool, + pub pin_button: button::State, + pub content: Content, + pub controls: Controls, +} + struct Content { id: usize, scroll: scrollable::State, @@ -220,6 +253,21 @@ struct Content { close: button::State, } +struct Controls { + close: button::State, +} + +impl Pane { + fn new(id: usize) -> Self { + Self { + is_pinned: false, + pin_button: button::State::new(), + content: Content::new(id), + controls: Controls::new(), + } + } +} + impl Content { fn new(id: usize) -> Self { Content { @@ -234,6 +282,7 @@ impl Content { &mut self, pane: pane_grid::Pane, total_panes: usize, + is_pinned: bool, ) -> Element<Message> { let Content { scroll, @@ -273,7 +322,7 @@ impl Content { style::Button::Primary, )); - if total_panes > 1 { + if total_panes > 1 && !is_pinned { controls = controls.push(button( close, "Close", @@ -297,7 +346,32 @@ impl Content { } } +impl Controls { + fn new() -> Self { + Self { + close: button::State::new(), + } + } + + pub fn view( + &mut self, + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + ) -> Element<Message> { + let mut button = + Button::new(&mut self.close, Text::new("Close").size(14)) + .style(style::Button::Control) + .padding(3); + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); + } + button.into() + } +} + mod style { + use crate::PANE_ID_COLOR_FOCUSED; use iced::{button, container, Background, Color, Vector}; const SURFACE: Color = Color::from_rgb( @@ -359,6 +433,8 @@ mod style { pub enum Button { Primary, Destructive, + Control, + Pin, } impl button::StyleSheet for Button { @@ -368,6 +444,8 @@ mod style { Button::Destructive => { (None, Color::from_rgb8(0xFF, 0x47, 0x47)) } + Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE), + Button::Pin => (Some(ACTIVE), Color::WHITE), }; button::Style { @@ -388,6 +466,8 @@ mod style { a: 0.2, ..active.text_color }), + Button::Control => Some(PANE_ID_COLOR_FOCUSED), + Button::Pin => Some(HOVERED), }; button::Style { diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 68662602..1eec9791 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -11,7 +11,7 @@ pub fn main() -> iced::Result { struct Example { scroll: scrollable::State, pick_list: pick_list::State<Language>, - selected_language: Language, + selected_language: Option<Language>, } #[derive(Debug, Clone, Copy)] @@ -33,7 +33,7 @@ impl Sandbox for Example { fn update(&mut self, message: Message) { match message { Message::LanguageSelected(language) => { - self.selected_language = language; + self.selected_language = Some(language); } } } @@ -42,9 +42,10 @@ impl Sandbox for Example { let pick_list = PickList::new( &mut self.pick_list, &Language::ALL[..], - Some(self.selected_language), + self.selected_language, Message::LanguageSelected, - ); + ) + .placeholder("Choose a language..."); let mut content = Scrollable::new(&mut self.scroll) .width(Length::Fill) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index d2f1bb62..da1d5d5d 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -213,7 +213,10 @@ impl Pokemon { } async fn fetch_image(id: u16) -> Result<image::Handle, reqwest::Error> { - let url = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); + let url = format!( + "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", + id + ); #[cfg(not(target_arch = "wasm32"))] { diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml index 12753fb6..08502458 100644 --- a/examples/scrollable/Cargo.toml +++ b/examples/scrollable/Cargo.toml @@ -6,4 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 8dd2e20c..3416b83d 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,8 +1,8 @@ mod style; use iced::{ - scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox, - Scrollable, Settings, Space, Text, + button, scrollable, Button, Column, Container, Element, Length, + ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text, }; pub fn main() -> iced::Result { @@ -17,6 +17,9 @@ struct ScrollableDemo { #[derive(Debug, Clone)] enum Message { ThemeChanged(style::Theme), + ScrollToTop(usize), + ScrollToBottom(usize), + Scrolled(usize, f32), } impl Sandbox for ScrollableDemo { @@ -36,6 +39,25 @@ impl Sandbox for ScrollableDemo { fn update(&mut self, message: Message) { match message { Message::ThemeChanged(theme) => self.theme = theme, + Message::ScrollToTop(i) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.scrollable.snap_to(0.0); + + variant.latest_offset = 0.0; + } + } + Message::ScrollToBottom(i) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.scrollable.snap_to(1.0); + + variant.latest_offset = 1.0; + } + } + Message::Scrolled(i, offset) => { + if let Some(variant) = self.variants.get_mut(i) { + variant.latest_offset = offset; + } + } } } @@ -62,13 +84,28 @@ impl Sandbox for ScrollableDemo { 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)); + .enumerate() + .map(|(i, variant)| { + let mut scrollable = + Scrollable::new(&mut variant.scrollable) + .padding(10) + .spacing(10) + .width(Length::Fill) + .height(Length::Fill) + .on_scroll(move |offset| { + Message::Scrolled(i, offset) + }) + .style(*theme) + .push(Text::new(variant.title)) + .push( + Button::new( + &mut variant.scroll_to_bottom, + Text::new("Scroll to bottom"), + ) + .width(Length::Fill) + .padding(10) + .on_press(Message::ScrollToBottom(i)), + ); if let Some(scrollbar_width) = variant.scrollbar_width { scrollable = scrollable @@ -108,12 +145,31 @@ impl Sandbox for ScrollableDemo { .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) + .push(Text::new("The End.")) + .push( + Button::new( + &mut variant.scroll_to_top, + Text::new("Scroll to top"), + ) + .width(Length::Fill) + .padding(10) + .on_press(Message::ScrollToTop(i)), + ); + + Column::new() .width(Length::Fill) .height(Length::Fill) - .style(*theme) + .spacing(10) + .push( + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .style(*theme), + ) + .push(ProgressBar::new( + 0.0..=1.0, + variant.latest_offset, + )) .into() }) .collect(), @@ -142,10 +198,13 @@ impl Sandbox for ScrollableDemo { /// A version of a scrollable struct Variant { title: &'static str, - state: scrollable::State, + scrollable: scrollable::State, + scroll_to_top: button::State, + scroll_to_bottom: button::State, scrollbar_width: Option<u16>, scrollbar_margin: Option<u16>, scroller_width: Option<u16>, + latest_offset: f32, } impl Variant { @@ -153,31 +212,43 @@ impl Variant { vec![ Self { title: "Default Scrollbar", - state: scrollable::State::new(), + scrollable: scrollable::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: None, scrollbar_margin: None, scroller_width: None, + latest_offset: 0.0, }, Self { title: "Slimmed & Margin", - state: scrollable::State::new(), + scrollable: scrollable::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: Some(3), scroller_width: Some(4), + latest_offset: 0.0, }, Self { title: "Wide Scroller", - state: scrollable::State::new(), + scrollable: scrollable::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: None, scroller_width: Some(10), + latest_offset: 0.0, }, Self { title: "Narrow Scroller", - state: scrollable::State::new(), + scrollable: scrollable::State::new(), + scroll_to_top: button::State::new(), + scroll_to_bottom: button::State::new(), scrollbar_width: Some(10), scrollbar_margin: None, scroller_width: Some(4), + latest_offset: 0.0, }, ] } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 8975fd9a..7bc49281 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,7 +1,7 @@ use iced::{ button, scrollable, slider, text_input, Align, Button, Checkbox, Column, Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox, - Scrollable, Settings, Slider, Space, Text, TextInput, + Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, }; pub fn main() -> iced::Result { @@ -17,7 +17,8 @@ struct Styling { button: button::State, slider: slider::State, slider_value: f32, - toggle_value: bool, + checkbox_value: bool, + toggler_value: bool, } #[derive(Debug, Clone)] @@ -27,6 +28,7 @@ enum Message { ButtonPressed, SliderChanged(f32), CheckboxToggled(bool), + TogglerToggled(bool), } impl Sandbox for Styling { @@ -44,9 +46,10 @@ impl Sandbox for Styling { match message { Message::ThemeChanged(theme) => self.theme = theme, Message::InputChanged(value) => self.input_value = value, - Message::ButtonPressed => (), + Message::ButtonPressed => {} Message::SliderChanged(value) => self.slider_value = value, - Message::CheckboxToggled(value) => self.toggle_value = value, + Message::CheckboxToggled(value) => self.checkbox_value = value, + Message::TogglerToggled(value) => self.toggler_value = value, } } @@ -101,11 +104,19 @@ impl Sandbox for Styling { .push(Text::new("You did it!")); let checkbox = Checkbox::new( - self.toggle_value, - "Toggle me!", + self.checkbox_value, + "Check me!", Message::CheckboxToggled, ) - .width(Length::Fill) + .style(self.theme); + + let toggler = Toggler::new( + self.toggler_value, + String::from("Toggle me!"), + Message::TogglerToggled, + ) + .width(Length::Shrink) + .spacing(10) .style(self.theme); let content = Column::new() @@ -124,7 +135,13 @@ impl Sandbox for Styling { .align_items(Align::Center) .push(scrollable) .push(Rule::vertical(38).style(self.theme)) - .push(checkbox), + .push( + Column::new() + .width(Length::Shrink) + .spacing(20) + .push(checkbox) + .push(toggler), + ), ); Container::new(content) @@ -140,7 +157,7 @@ impl Sandbox for Styling { mod style { use iced::{ button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, + slider, text_input, toggler, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -231,6 +248,15 @@ mod style { } } + impl From<Theme> for Box<dyn toggler::StyleSheet> { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Toggler.into(), + } + } + } + impl From<Theme> for Box<dyn rule::StyleSheet> { fn from(theme: Theme) -> Self { match theme { @@ -269,7 +295,7 @@ mod style { mod dark { use iced::{ button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, Color, + slider, text_input, toggler, Color, }; const SURFACE: Color = Color::from_rgb( @@ -520,6 +546,35 @@ mod style { } } + pub struct Toggler; + + impl toggler::StyleSheet for Toggler { + fn active(&self, is_active: bool) -> toggler::Style { + toggler::Style { + background: if is_active { ACTIVE } else { SURFACE }, + background_border: None, + foreground: if is_active { Color::WHITE } else { ACTIVE }, + foreground_border: None, + } + } + + fn hovered(&self, is_active: bool) -> toggler::Style { + toggler::Style { + background: if is_active { ACTIVE } else { SURFACE }, + background_border: None, + foreground: if is_active { + Color { + a: 0.5, + ..Color::WHITE + } + } else { + Color { a: 0.5, ..ACTIVE } + }, + foreground_border: None, + } + } + } + pub struct Rule; impl rule::StyleSheet for Rule { diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 7186b950..97415475 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -265,8 +265,11 @@ impl Task { self.completed = completed; } TaskMessage::Edit => { + let mut text_input = text_input::State::focused(); + text_input.select_all(); + self.state = TaskState::Editing { - text_input: text_input::State::focused(), + text_input, delete_button: button::State::new(), }; } diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index e8755d39..1215f83d 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,7 +1,7 @@ use iced::{ button, scrollable, slider, text_input, Button, Checkbox, Color, Column, Container, Element, HorizontalAlignment, Image, Length, Radio, Row, - Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, + Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, }; pub fn main() -> iced::Result { @@ -135,6 +135,9 @@ impl Steps { color: Color::BLACK, }, Step::Radio { selection: None }, + Step::Toggler { + can_continue: false, + }, Step::Image { width: 300, slider: slider::State::new(), @@ -206,6 +209,9 @@ enum Step { Radio { selection: Option<Language>, }, + Toggler { + can_continue: bool, + }, Image { width: u16, slider: slider::State, @@ -232,6 +238,7 @@ pub enum StepMessage { InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), + TogglerChanged(bool), } impl<'a> Step { @@ -287,6 +294,11 @@ impl<'a> Step { *is_secure = toggle; } } + StepMessage::TogglerChanged(value) => { + if let Step::Toggler { can_continue, .. } = self { + *can_continue = value; + } + } }; } @@ -294,6 +306,7 @@ impl<'a> Step { match self { Step::Welcome => "Welcome", Step::Radio { .. } => "Radio button", + Step::Toggler { .. } => "Toggler", Step::Slider { .. } => "Slider", Step::Text { .. } => "Text", Step::Image { .. } => "Image", @@ -309,6 +322,7 @@ impl<'a> Step { match self { Step::Welcome => true, Step::Radio { selection } => *selection == Some(Language::Rust), + Step::Toggler { can_continue } => *can_continue, Step::Slider { .. } => true, Step::Text { .. } => true, Step::Image { .. } => true, @@ -324,6 +338,7 @@ impl<'a> Step { match self { Step::Welcome => Self::welcome(), Step::Radio { selection } => Self::radio(*selection), + Step::Toggler { can_continue } => Self::toggler(*can_continue), Step::Slider { state, value } => Self::slider(state, *value), Step::Text { size_slider, @@ -545,6 +560,21 @@ impl<'a> Step { )) } + fn toggler(can_continue: bool) -> Column<'a, StepMessage> { + Self::container("Toggler") + .push(Text::new( + "A toggler is mostly used to enable or disable something.", + )) + .push( + Container::new(Toggler::new( + can_continue, + String::from("Toggle me to continue..."), + StepMessage::TogglerChanged, + )) + .padding([0, 40]), + ) + } + fn image( width: u16, slider: &'a mut slider::State, diff --git a/examples/url_handler/Cargo.toml b/examples/url_handler/Cargo.toml new file mode 100644 index 00000000..911b2f25 --- /dev/null +++ b/examples/url_handler/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "url_handler" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" }
\ No newline at end of file diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs new file mode 100644 index 00000000..f14e5227 --- /dev/null +++ b/examples/url_handler/src/main.rs @@ -0,0 +1,73 @@ +use iced::{ + executor, Application, Clipboard, Command, Container, Element, Length, + Settings, Subscription, Text, +}; +use iced_native::{ + event::{MacOS, PlatformSpecific}, + Event, +}; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +#[derive(Debug, Default)] +struct App { + url: Option<String>, +} + +#[derive(Debug, Clone)] +enum Message { + EventOccurred(iced_native::Event), +} + +impl Application for App { + type Executor = executor::Default; + type Message = Message; + type Flags = (); + + fn new(_flags: ()) -> (App, Command<Message>) { + (App::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Url - Iced") + } + + fn update( + &mut self, + message: Message, + _clipboard: &mut Clipboard, + ) -> Command<Message> { + match message { + Message::EventOccurred(event) => { + if let Event::PlatformSpecific(PlatformSpecific::MacOS( + MacOS::ReceivedUrl(url), + )) = event + { + self.url = Some(url); + } + } + }; + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + iced_native::subscription::events().map(Message::EventOccurred) + } + + fn view(&mut self) -> Element<Message> { + let content = match &self.url { + Some(url) => Text::new(format!("{}", url)), + None => Text::new("No URL received yet!"), + }; + + Container::new(content.size(48)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/futures/Cargo.toml b/futures/Cargo.toml index c266f705..3cea6e1a 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_futures" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "Commands, subscriptions, and runtimes for Iced" diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 27d2d295..e60ad79a 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -125,9 +125,9 @@ 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.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 +/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.3/examples/download_progress +/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.3/examples/stopwatch pub trait Recipe<Hasher: std::hash::Hasher, Event> { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. diff --git a/glow/Cargo.toml b/glow/Cargo.toml index 3f85e52d..e40b8ba8 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glow" -version = "0.1.0" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A glow renderer for iced" @@ -24,11 +24,11 @@ bytemuck = "1.4" log = "0.4" [dependencies.iced_native] -version = "0.3" +version = "0.4" path = "../native" [dependencies.iced_graphics] -version = "0.1" +version = "0.2" path = "../graphics" features = ["font-fallback", "font-icons", "opengl"] diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 92bb993e..37c0ac9d 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -2,6 +2,7 @@ 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; @@ -24,7 +25,12 @@ pub struct Backend { 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 text_pipeline = text::Pipeline::new( + gl, + settings.default_font, + settings.text_multithreading, + ); + let quad_pipeline = quad::Pipeline::new(gl); let triangle_pipeline = triangle::Pipeline::new(gl); @@ -206,6 +212,25 @@ impl backend::Text for Backend { ) -> (f32, f32) { self.text_pipeline.measure(contents, size, font, bounds) } + + fn hit_test( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + point: iced_native::Point, + nearest_only: bool, + ) -> text::Hit { + self.text_pipeline.hit_test( + contents, + size, + font, + bounds, + point, + nearest_only, + ) + } } #[cfg(feature = "image")] diff --git a/glow/src/lib.rs b/glow/src/lib.rs index 98faf24c..888492d8 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -4,14 +4,14 @@ //! //! [`glow`]: https://github.com/grovesNL/glow //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -#![deny(missing_docs)] +//#![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] #![forbid(rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg))] mod backend; -mod program; +pub mod program; mod quad; mod text; mod triangle; diff --git a/glow/src/settings.rs b/glow/src/settings.rs index 524d91a9..f3dddfaf 100644 --- a/glow/src/settings.rs +++ b/glow/src/settings.rs @@ -16,7 +16,15 @@ pub struct Settings { /// By default, it will be set to 20. pub default_text_size: u16, + /// If enabled, spread text workload in multiple threads when multiple cores + /// are available. + /// + /// By default, it is disabled. + pub text_multithreading: bool, + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. pub antialiasing: Option<Antialiasing>, } @@ -25,7 +33,17 @@ impl Default for Settings { Settings { default_font: None, default_text_size: 20, + text_multithreading: false, antialiasing: None, } } } + +impl Settings { + /// Creates new [`Settings`] using environment configuration. + /// + /// Currently, this is equivalent to calling [`Settings::default`]. + pub fn from_env() -> Self { + Self::default() + } +} diff --git a/glow/src/text.rs b/glow/src/text.rs index 925c7287..d6915d92 100644 --- a/glow/src/text.rs +++ b/glow/src/text.rs @@ -1,8 +1,12 @@ use crate::Transformation; -use glow_glyph::ab_glyph; + use iced_graphics::font; + +use glow_glyph::ab_glyph; use std::{cell::RefCell, collections::HashMap}; +pub use iced_native::text::Hit; + #[derive(Debug)] pub struct Pipeline { draw_brush: RefCell<glow_glyph::GlyphBrush>, @@ -11,7 +15,11 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self { + pub fn new( + gl: &glow::Context, + default_font: Option<&[u8]>, + multithreading: bool, + ) -> Self { let default_font = default_font.map(|slice| slice.to_vec()); // TODO: Font customization @@ -41,7 +49,7 @@ impl Pipeline { 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 + .draw_cache_multithread(multithreading) .build(&gl); let measure_brush = @@ -105,6 +113,94 @@ impl Pipeline { } } + pub fn hit_test( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, + nearest_only: bool, + ) -> Hit { + 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() + }; + + let mut mb = self.measure_brush.borrow_mut(); + + // The underlying type is FontArc, so clones are cheap. + use ab_glyph::{Font, ScaleFont}; + let font = mb.fonts()[font_id].clone().into_scaled(size); + + // Implements an iterator over the glyph bounding boxes. + let bounds = mb.glyphs(section).map( + |glow_glyph::SectionGlyph { + byte_index, glyph, .. + }| { + ( + *byte_index, + iced_native::Rectangle::new( + iced_native::Point::new( + glyph.position.x - font.h_side_bearing(glyph.id), + glyph.position.y - font.ascent(), + ), + iced_native::Size::new( + font.h_advance(glyph.id), + font.ascent() - font.descent(), + ), + ), + ) + }, + ); + + // Implements computation of the character index based on the byte index + // within the input string. + let char_index = |byte_index| { + let mut b_count = 0; + for (i, utf8_len) in + content.chars().map(|c| c.len_utf8()).enumerate() + { + if byte_index < (b_count + utf8_len) { + return i; + } + b_count += utf8_len; + } + return byte_index; + }; + + if !nearest_only { + for (idx, bounds) in bounds.clone() { + if bounds.contains(point) { + return Hit::CharOffset(char_index(idx)); + } + } + } + + let (idx, nearest) = bounds.fold( + (0usize, iced_native::Point::ORIGIN), + |acc: (usize, iced_native::Point), (idx, bounds)| { + if bounds.center().distance(point) < acc.1.distance(point) { + (idx, bounds.center()) + } else { + acc + } + }, + ); + + Hit::NearestCharOffset(char_index(idx), (point - nearest).into()) + } + 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. diff --git a/glow/src/widget.rs b/glow/src/widget.rs index 5481216a..a77511e8 100644 --- a/glow/src/widget.rs +++ b/glow/src/widget.rs @@ -20,6 +20,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; pub mod tooltip; #[doc(no_inline)] @@ -45,6 +46,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; #[cfg(feature = "canvas")] diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs index 44f9201c..fc36862c 100644 --- a/glow/src/widget/pane_grid.rs +++ b/glow/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! 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 +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid use crate::Renderer; pub use iced_graphics::pane_grid::{ diff --git a/glow/src/widget/toggler.rs b/glow/src/widget/toggler.rs new file mode 100644 index 00000000..1cd8711b --- /dev/null +++ b/glow/src/widget/toggler.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using togglers. +use crate::Renderer; + +pub use iced_graphics::toggler::{Style, StyleSheet}; + +/// A toggler that can be toggled. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Toggler<Message> = iced_native::Toggler<Message, Renderer>; diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 505ee7e5..4300d77c 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glutin" -version = "0.1.0" +version = "0.2.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A glutin runtime for Iced" @@ -13,18 +13,20 @@ categories = ["gui"] [features] debug = ["iced_winit/debug"] -[dependencies] -glutin = "0.26" +[dependencies.glutin] +version = "0.27" +git = "https://github.com/iced-rs/glutin" +rev = "849352f1cb7021b7514a3104daf3e6236c915dac" [dependencies.iced_native] -version = "0.3" +version = "0.4" path = "../native" [dependencies.iced_winit] -version = "0.2" +version = "0.3" path = "../winit" [dependencies.iced_graphics] -version = "0.1" +version = "0.2" path = "../graphics" features = ["opengl"] diff --git a/glutin/README.md b/glutin/README.md index addb9228..fcae157e 100644 --- a/glutin/README.md +++ b/glutin/README.md @@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t Add `iced_glutin` as a dependency in your `Cargo.toml`: ```toml -iced_glutin = "0.1" +iced_glutin = "0.2" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 3a08104e..4a5f4bd2 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -52,11 +52,15 @@ where runtime.track(subscription); let context = { - let builder = settings.window.into_builder( - &application.title(), - application.mode(), - event_loop.primary_monitor(), - ); + let builder = settings + .window + .into_builder( + &application.title(), + application.mode(), + event_loop.primary_monitor(), + settings.id, + ) + .with_menu(Some(conversion::menu(&application.menu()))); let context = ContextBuilder::new() .with_vsync(true) @@ -92,10 +96,11 @@ where application, compositor, renderer, - context, runtime, debug, receiver, + context, + settings.exit_on_close_request, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -107,7 +112,22 @@ where return; } - if let Some(event) = event.to_static() { + let event = match event { + glutin::event::Event::WindowEvent { + event: + glutin::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + }, + window_id, + } => Some(glutin::event::Event::WindowEvent { + event: glutin::event::WindowEvent::Resized(*new_inner_size), + window_id, + }), + _ => event.to_static(), + }; + + if let Some(event) = event { sender.start_send(event).expect("Send event"); let poll = instance.as_mut().poll(&mut context); @@ -124,10 +144,11 @@ 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>>, + mut context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>, + exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -197,6 +218,8 @@ async fn run_instance<A, E, C>( // Update window state.synchronize(&application, context.window()); + let should_exit = application.should_exit(); + user_interface = ManuallyDrop::new(application::build_user_interface( &mut application, @@ -205,6 +228,10 @@ async fn run_instance<A, E, C>( state.logical_size(), &mut debug, )); + + if should_exit { + break; + } } debug.draw_started(); @@ -214,11 +241,31 @@ async fn run_instance<A, E, C>( context.window().request_redraw(); } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use iced_native::event; + events.push(iced_native::Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } event::Event::UserEvent(message) => { messages.push(message); } event::Event::RedrawRequested(_) => { debug.render_started(); + + #[allow(unsafe_code)] + unsafe { + if !context.is_current() { + context = context + .make_current() + .expect("Make OpenGL context current"); + } + } + let current_viewport_version = state.viewport_version(); if viewport_version != current_viewport_version { @@ -271,10 +318,21 @@ async fn run_instance<A, E, C>( // Maybe we can use `ControlFlow::WaitUntil` for this. } event::Event::WindowEvent { + event: event::WindowEvent::MenuEntryActivated(entry_id), + .. + } => { + if let Some(message) = + conversion::menu_message(state.menu(), entry_id) + { + messages.push(message); + } + } + event::Event::WindowEvent { event: window_event, .. } => { if application::requests_exit(&window_event, state.modifiers()) + && exit_on_close_request { break; } diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index ab41ac44..ea9471c6 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_graphics" -version = "0.1.0" +version = "0.2.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" @@ -28,11 +28,11 @@ version = "1.4" features = ["derive"] [dependencies.iced_native] -version = "0.3" +version = "0.4" path = "../native" [dependencies.iced_style] -version = "0.2" +version = "0.3" path = "../style" [dependencies.lyon] diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index ed1b9e08..656949c5 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -1,7 +1,8 @@ //! Write a graphics backend. use iced_native::image; use iced_native::svg; -use iced_native::{Font, Size}; +use iced_native::text; +use iced_native::{Font, Point, Size}; /// The graphics backend of a [`Renderer`]. /// @@ -43,6 +44,23 @@ pub trait Text { font: Font, bounds: Size, ) -> (f32, f32); + + /// Tests whether the provided point is within the boundaries of [`Text`] + /// laid out with the given parameters, returning information about + /// the nearest character. + /// + /// If nearest_only is true, the hit test does not consider whether the + /// the point is interior to any glyph bounds, returning only the character + /// with the nearest centeroid. + fn hit_test( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> text::Hit; } /// A graphics backend that supports image rendering. diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs index ffe998c5..9e91a0ef 100644 --- a/graphics/src/overlay/menu.rs +++ b/graphics/src/overlay/menu.rs @@ -2,8 +2,8 @@ use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::{ - mouse, overlay, Color, Font, HorizontalAlignment, Point, Rectangle, - VerticalAlignment, + mouse, overlay, Color, Font, HorizontalAlignment, Padding, Point, + Rectangle, VerticalAlignment, }; pub use iced_style::menu::Style; @@ -45,7 +45,7 @@ where viewport: &Rectangle, options: &[T], hovered_option: Option<usize>, - padding: u16, + padding: Padding, text_size: u16, font: Font, style: &Style, @@ -53,7 +53,7 @@ where use std::f32; let is_mouse_over = bounds.contains(cursor_position); - let option_height = text_size as usize + padding as usize * 2; + let option_height = (text_size + padding.vertical()) as usize; let mut primitives = Vec::new(); @@ -72,7 +72,7 @@ where x: bounds.x, y: bounds.y + (option_height * i) as f32, width: bounds.width, - height: f32::from(text_size + padding * 2), + height: f32::from(text_size + padding.vertical()), }; if is_selected { @@ -88,7 +88,7 @@ where primitives.push(Primitive::Text { content: option.to_string(), bounds: Rectangle { - x: bounds.x + f32::from(padding), + x: bounds.x + padding.left as f32, y: bounds.center_y(), width: f32::INFINITY, ..bounds diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index 190ea9c0..e34d267f 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -20,6 +20,7 @@ pub mod scrollable; pub mod slider; pub mod svg; pub mod text_input; +pub mod toggler; pub mod tooltip; mod column; @@ -50,6 +51,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; pub use column::Column; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs index 2e3f78ca..60400ed8 100644 --- a/graphics/src/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -5,7 +5,7 @@ use crate::defaults::{self, Defaults}; use crate::{Backend, Primitive, Renderer}; use iced_native::mouse; use iced_native::{ - Background, Color, Element, Layout, Point, Rectangle, Vector, + Background, Color, Element, Layout, Padding, Point, Rectangle, Vector, }; pub use iced_native::button::State; @@ -21,7 +21,7 @@ impl<B> iced_native::button::Renderer for Renderer<B> where B: Backend, { - const DEFAULT_PADDING: u16 = 5; + const DEFAULT_PADDING: Padding = Padding::new(5); type Style = Box<dyn StyleSheet>; diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index b86f9e04..5af9d11f 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -54,7 +54,7 @@ impl Frame { self.size.width } - /// Returns the width of the [`Frame`]. + /// Returns the height of the [`Frame`]. #[inline] pub fn height(&self) -> f32 { self.size.height diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index d703caad..85a2f67b 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -34,7 +34,7 @@ pub trait Program<Message> { /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a /// [`Cache`]. /// - /// [`Frame`]: crate::widget::canvas::Cache + /// [`Frame`]: crate::widget::canvas::Frame /// [`Cache`]: crate::widget::canvas::Cache fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>; diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs index d06f8c6c..92cdbb77 100644 --- a/graphics/src/widget/pane_grid.rs +++ b/graphics/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! 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 +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid use crate::defaults; use crate::{Backend, Color, Primitive, Renderer}; use iced_native::container; @@ -218,10 +218,10 @@ where body_primitive, ], }, - if is_over_pick_area { - mouse::Interaction::Grab - } else if title_bar_interaction > body_interaction { + if title_bar_interaction > body_interaction { title_bar_interaction + } else if is_over_pick_area { + mouse::Interaction::Grab } else { body_interaction }, diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs index f42a8707..88a590b5 100644 --- a/graphics/src/widget/pick_list.rs +++ b/graphics/src/widget/pick_list.rs @@ -2,7 +2,8 @@ use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; use iced_native::{ - mouse, Font, HorizontalAlignment, Point, Rectangle, VerticalAlignment, + mouse, Font, HorizontalAlignment, Padding, Point, Rectangle, + VerticalAlignment, }; use iced_style::menu; @@ -19,7 +20,7 @@ where { type Style = Box<dyn StyleSheet>; - const DEFAULT_PADDING: u16 = 5; + const DEFAULT_PADDING: Padding = Padding::new(5); fn menu_style(style: &Box<dyn StyleSheet>) -> menu::Style { style.menu() @@ -30,12 +31,14 @@ where bounds: Rectangle, cursor_position: Point, selected: Option<String>, - padding: u16, + placeholder: Option<&str>, + padding: Padding, text_size: u16, font: Font, style: &Box<dyn StyleSheet>, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); + let is_selected = selected.is_some(); let style = if is_mouse_over { style.hovered() @@ -56,7 +59,7 @@ where font: B::ICON_FONT, size: bounds.height * style.icon_size, bounds: Rectangle { - x: bounds.x + bounds.width - f32::from(padding) * 2.0, + x: bounds.x + bounds.width - f32::from(padding.horizontal()), y: bounds.center_y(), ..bounds }, @@ -67,14 +70,18 @@ where ( Primitive::Group { - primitives: if let Some(label) = selected { + primitives: if let Some(label) = + selected.or_else(|| placeholder.map(str::to_string)) + { let label = Primitive::Text { content: label, size: f32::from(text_size), font, - color: style.text_color, + color: is_selected + .then(|| style.text_color) + .unwrap_or(style.placeholder_color), bounds: Rectangle { - x: bounds.x + f32::from(padding), + x: bounds.x + f32::from(padding.left), y: bounds.center_y(), ..bounds }, diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs index 57065ba2..2220e4b8 100644 --- a/graphics/src/widget/scrollable.rs +++ b/graphics/src/widget/scrollable.rs @@ -134,8 +134,16 @@ where Primitive::None }; + let scroll = Primitive::Clip { + bounds, + offset: Vector::new(0, 0), + content: Box::new(Primitive::Group { + primitives: vec![scrollbar, scroller], + }), + }; + Primitive::Group { - primitives: vec![clip, scrollbar, scroller], + primitives: vec![clip, scroll], } } else { content diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs index 7e22e680..c235f254 100644 --- a/graphics/src/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -4,7 +4,7 @@ use crate::{Primitive, Renderer}; use iced_native::mouse; use iced_native::text; use iced_native::{ - Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment, + Color, Font, HorizontalAlignment, Point, Rectangle, Size, VerticalAlignment, }; /// A paragraph of text. @@ -35,6 +35,25 @@ where .measure(content, f32::from(size), font, bounds) } + fn hit_test( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> text::Hit { + self.backend().hit_test( + content, + size, + font, + bounds, + point, + nearest_only, + ) + } + fn draw( &mut self, defaults: &Self::Defaults, diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs new file mode 100644 index 00000000..852d18ee --- /dev/null +++ b/graphics/src/widget/toggler.rs @@ -0,0 +1,99 @@ +//! Show toggle controls using togglers. +use crate::backend::{self, Backend}; +use crate::{Primitive, Renderer}; +use iced_native::mouse; +use iced_native::toggler; +use iced_native::Rectangle; + +pub use iced_style::toggler::{Style, StyleSheet}; + +/// Makes sure that the border radius of the toggler looks good at every size. +const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; + +/// The space ratio between the background Quad and the Toggler bounds, and +/// between the background Quad and foreground Quad. +const SPACE_RATIO: f32 = 0.05; + +/// A toggler that can be toggled. +/// +/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. +pub type Toggler<Message, Backend> = + iced_native::Toggler<Message, Renderer<Backend>>; + +impl<B> toggler::Renderer for Renderer<B> +where + B: Backend + backend::Text, +{ + type Style = Box<dyn StyleSheet>; + + const DEFAULT_SIZE: u16 = 20; + + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + label: Option<Self::Output>, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = if is_mouse_over { + style_sheet.hovered(is_active) + } else { + style_sheet.active(is_active) + }; + + let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO; + let space = SPACE_RATIO * bounds.height as f32; + + let toggler_background_bounds = Rectangle { + x: bounds.x + space, + y: bounds.y + space, + width: bounds.width - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + let toggler_background = Primitive::Quad { + bounds: toggler_background_bounds, + background: style.background.into(), + border_radius, + border_width: 1.0, + border_color: style.background_border.unwrap_or(style.background), + }; + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + if is_active { + bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + } else { + 2.0 * space + }, + y: bounds.y + (2.0 * space), + width: bounds.height - (4.0 * space), + height: bounds.height - (4.0 * space), + }; + + let toggler_foreground = Primitive::Quad { + bounds: toggler_foreground_bounds, + background: style.foreground.into(), + border_radius, + border_width: 1.0, + border_color: style.foreground_border.unwrap_or(style.foreground), + }; + + ( + Primitive::Group { + primitives: match label { + Some((l, _)) => { + vec![l, toggler_background, toggler_foreground] + } + None => vec![toggler_background, toggler_foreground], + }, + }, + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + }, + ) + } +} diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs index 1a1b5352..493a6389 100644 --- a/graphics/src/widget/tooltip.rs +++ b/graphics/src/widget/tooltip.rs @@ -5,7 +5,7 @@ use crate::{Primitive, Renderer, Vector}; use iced_native::container; use iced_native::layout::{self, Layout}; -use iced_native::{Element, Point, Rectangle, Size, Text}; +use iced_native::{Element, Padding, Point, Rectangle, Size, Text}; /// An element decorating some content. /// @@ -49,7 +49,6 @@ where use iced_native::Widget; let gap = f32::from(gap); - let padding = f32::from(padding); let style = style_sheet.style(); let defaults = Defaults { @@ -62,9 +61,10 @@ where tooltip, self, &layout::Limits::new(Size::ZERO, viewport.size()) - .pad(f32::from(padding)), + .pad(Padding::new(padding)), ); + let padding = f32::from(padding); let text_bounds = text_layout.bounds(); let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; let y_center = diff --git a/graphics/src/window.rs b/graphics/src/window.rs index 3e74db5f..67ec3322 100644 --- a/graphics/src/window.rs +++ b/graphics/src/window.rs @@ -4,7 +4,7 @@ mod compositor; #[cfg(feature = "opengl")] mod gl_compositor; -pub use compositor::Compositor; +pub use compositor::{Compositor, SurfaceError}; #[cfg(feature = "opengl")] pub use gl_compositor::GLCompositor; diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index 0bc8cbc8..37edef1d 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -1,6 +1,9 @@ use crate::{Color, Error, Viewport}; + use iced_native::mouse; + use raw_window_handle::HasRawWindowHandle; +use thiserror::Error; /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { @@ -13,11 +16,11 @@ pub trait Compositor: Sized { /// The surface of the backend. type Surface; - /// The swap chain of the backend. - type SwapChain; - /// Creates a new [`Compositor`]. - fn new(settings: Self::Settings) -> Result<(Self, Self::Renderer), Error>; + fn new<W: HasRawWindowHandle>( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error>; /// Crates a new [`Surface`] for the given window. /// @@ -31,12 +34,12 @@ pub trait Compositor: Sized { /// /// [`SwapChain`]: Self::SwapChain /// [`Surface`]: Self::Surface - fn create_swap_chain( + fn configure_surface( &mut self, - surface: &Self::Surface, + surface: &mut Self::Surface, width: u32, height: u32, - ) -> Self::SwapChain; + ); /// Draws the output primitives to the next frame of the given [`SwapChain`]. /// @@ -44,10 +47,31 @@ pub trait Compositor: Sized { fn draw<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - swap_chain: &mut Self::SwapChain, + surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, output: &<Self::Renderer as iced_native::Renderer>::Output, overlay: &[T], - ) -> mouse::Interaction; + ) -> Result<mouse::Interaction, SurfaceError>; +} + +/// Result of an unsuccessful call to [`Compositor::draw`]. +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum SurfaceError { + /// A timeout was encountered while trying to acquire the next frame. + #[error( + "A timeout was encountered while trying to acquire the next frame" + )] + Timeout, + /// The underlying surface has changed, and therefore the swap chain must be updated. + #[error( + "The underlying surface has changed, and therefore the swap chain must be updated." + )] + Outdated, + /// The swap chain has been lost and needs to be recreated. + #[error("The swap chain has been lost and needs to be recreated")] + Lost, + /// There is no more memory left to allocate a new frame. + #[error("There is no more memory left to allocate a new frame")] + OutOfMemory, } diff --git a/native/Cargo.toml b/native/Cargo.toml index 2c99638a..a3134ef4 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A renderer-agnostic library for native GUIs" @@ -16,10 +16,10 @@ unicode-segmentation = "1.6" num-traits = "0.2" [dependencies.iced_core] -version = "0.3" +version = "0.4" path = "../core" [dependencies.iced_futures] -version = "0.2" +version = "0.3" path = "../futures" features = ["thread-pool"] diff --git a/native/README.md b/native/README.md index 6323dd4f..0d79690a 100644 --- a/native/README.md +++ b/native/README.md @@ -28,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.3" +iced_native = "0.4" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/native/src/event.rs b/native/src/event.rs index 205bb797..1c26b5f2 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -23,6 +23,27 @@ pub enum Event { /// A touch event Touch(touch::Event), + + /// A platform specific event + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), } /// The status of an [`Event`] after being processed. diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 4f6523fb..3d3ff82c 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,9 +16,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + use crate::{ layout::{Limits, Node}, - Align, Element, Point, Size, + Align, Element, Padding, Point, Size, }; /// The main axis of a flex layout. @@ -62,7 +63,7 @@ pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, limits: &Limits, - padding: f32, + padding: Padding, spacing: f32, align_items: Align, items: &[Element<'_, Message, Renderer>], @@ -141,14 +142,15 @@ where } } - let mut main = padding; + let pad = axis.pack(padding.left as f32, padding.top as f32); + let mut main = pad.0; for (i, node) in nodes.iter_mut().enumerate() { if i > 0 { main += spacing; } - let (x, y) = axis.pack(main, padding); + let (x, y) = axis.pack(main, pad.1); node.move_to(Point::new(x, y)); @@ -166,7 +168,7 @@ where main += axis.main(size); } - let (width, height) = axis.pack(main - padding, cross); + let (width, height) = axis.pack(main - pad.0, cross); let size = limits.resolve(Size::new(width, height)); Node::with_children(size.pad(padding), nodes) diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index a7bb5c9c..6d5f6563 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -1,4 +1,4 @@ -use crate::{Length, Size}; +use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. #[derive(Debug, Clone, Copy)] @@ -117,8 +117,11 @@ impl Limits { } /// Shrinks the current [`Limits`] to account for the given padding. - pub fn pad(&self, padding: f32) -> Limits { - self.shrink(Size::new(padding * 2.0, padding * 2.0)) + pub fn pad(&self, padding: Padding) -> Limits { + self.shrink(Size::new( + padding.horizontal() as f32, + padding.vertical() as f32, + )) } /// Shrinks the current [`Limits`] by the given [`Size`]. diff --git a/native/src/lib.rs b/native/src/lib.rs index 20bbb1d0..cbb02506 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -61,8 +61,8 @@ mod debug; mod debug; pub use iced_core::{ - Align, Background, Color, Font, HorizontalAlignment, Length, Point, - Rectangle, Size, Vector, VerticalAlignment, + menu, Align, Background, Color, Font, HorizontalAlignment, Length, Menu, + Padding, Point, Rectangle, Size, Vector, VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index afb17bd3..f62dcb46 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -8,8 +8,8 @@ use crate::scrollable; use crate::text; use crate::touch; use crate::{ - Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle, - Scrollable, Size, Vector, Widget, + Clipboard, Container, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Scrollable, Size, Vector, Widget, }; /// A list of selectable options. @@ -20,7 +20,7 @@ pub struct Menu<'a, T, Renderer: self::Renderer> { hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, width: u16, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, @@ -45,7 +45,7 @@ where hovered_option, last_selection, width: 0, - padding: 0, + padding: Padding::ZERO, text_size: None, font: Default::default(), style: Default::default(), @@ -58,9 +58,9 @@ where self } - /// Sets the padding of the [`Menu`]. - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Menu`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -261,7 +261,7 @@ struct List<'a, T, Renderer: self::Renderer> { options: &'a [T], hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, @@ -294,7 +294,7 @@ where let size = { let intrinsic = Size::new( 0.0, - f32::from(text_size + self.padding * 2) + f32::from(text_size + self.padding.vertical()) * self.options.len() as f32, ); @@ -345,7 +345,7 @@ where *self.hovered_option = Some( ((cursor_position.y - bounds.y) - / f32::from(text_size + self.padding * 2)) + / f32::from(text_size + self.padding.vertical())) as usize, ); } @@ -359,7 +359,7 @@ where *self.hovered_option = Some( ((cursor_position.y - bounds.y) - / f32::from(text_size + self.padding * 2)) + / f32::from(text_size + self.padding.vertical())) as usize, ); @@ -430,7 +430,7 @@ pub trait Renderer: viewport: &Rectangle, options: &[T], hovered_option: Option<usize>, - padding: u16, + padding: Padding, text_size: u16, font: Self::Font, style: &<Self as Renderer>::Style, diff --git a/native/src/program.rs b/native/src/program.rs index 066c29d8..75fab094 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -11,7 +11,7 @@ pub trait Program: Sized { type Renderer: Renderer; /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Clone + Send; /// The type of [`Clipboard`] your [`Program`] will use. type Clipboard: Clipboard; diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 9e91d29f..b1a26c41 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,8 +1,8 @@ use crate::{ 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, + scrollable, slider, text, text_input, toggler, Color, Element, Font, + HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size, + Vector, VerticalAlignment, }; /// A renderer that does nothing. @@ -67,6 +67,18 @@ impl text::Renderer for Null { (0.0, 20.0) } + fn hit_test( + &self, + _contents: &str, + _size: f32, + _font: Self::Font, + _bounds: Size, + _point: Point, + _nearest_only: bool, + ) -> text::Hit { + text::Hit::NearestCharOffset(0, Vector::new(0., 0.)) + } + fn draw( &mut self, _defaults: &Self::Defaults, @@ -145,7 +157,7 @@ impl text_input::Renderer for Null { } impl button::Renderer for Null { - const DEFAULT_PADDING: u16 = 0; + const DEFAULT_PADDING: Padding = Padding::ZERO; type Style = (); @@ -288,3 +300,19 @@ impl pane_grid::Renderer for Null { ) { } } + +impl toggler::Renderer for Null { + type Style = (); + + const DEFAULT_SIZE: u16 = 20; + + fn draw( + &mut self, + _bounds: Rectangle, + _is_checked: bool, + _is_mouse_over: bool, + _label: Option<Self::Output>, + _style: &Self::Style, + ) { + } +} diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 475faf8d..8e0d7d1c 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -16,7 +16,7 @@ use std::hash::Hasher; /// The [`integration` example] uses a [`UserInterface`] to integrate Iced in /// an existing graphical application. /// -/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration +/// [`integration` example]: https://github.com/hecrj/iced/tree/0.3/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, diff --git a/native/src/widget.rs b/native/src/widget.rs index 791c53a3..43c1b023 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -36,6 +36,7 @@ pub mod space; pub mod svg; pub mod text; pub mod text_input; +pub mod toggler; pub mod tooltip; #[doc(no_inline)] @@ -73,6 +74,8 @@ pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; use crate::event::{self, Event}; @@ -96,12 +99,12 @@ use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [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 +/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 99e98fd1..c469a0e5 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -7,7 +7,8 @@ use crate::mouse; use crate::overlay; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Widget, }; use std::hash::Hash; @@ -28,6 +29,29 @@ use std::hash::Hash; /// let button = Button::new(&mut state, Text::new("Press me!")) /// .on_press(Message::ButtonPressed); /// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # use iced_native::{button, Text}; +/// # +/// # type Button<'a, Message> = +/// # iced_native::Button<'a, Message, iced_native::renderer::Null>; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { +/// Button::new(state, Text::new("I'm disabled!")) +/// } +/// +/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { +/// disabled_button(state).on_press(Message::ButtonPressed) +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Button<'a, Message, Renderer: self::Renderer> { state: &'a mut State, @@ -37,7 +61,7 @@ pub struct Button<'a, Message, Renderer: self::Renderer> { height: Length, min_width: u32, min_height: u32, - padding: u16, + padding: Padding, style: Renderer::Style, } @@ -89,13 +113,14 @@ where self } - /// Sets the padding of the [`Button`]. - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } /// Sets the message that will be produced when the [`Button`] is pressed. + /// If on_press isn't set, button will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self @@ -140,18 +165,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); let limits = limits .min_width(self.min_width) .min_height(self.min_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - let size = limits.resolve(content.size()).pad(padding); + let size = limits.resolve(content.size()).pad(self.padding); layout::Node::with_children(size, vec![content]) } @@ -258,7 +285,7 @@ where /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer + Sized { /// The default padding of a [`Button`]. - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The style supported by this renderer. type Style: Default; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 6ce2e973..0f21c873 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -8,8 +8,8 @@ use crate::row; use crate::text; use crate::touch; use crate::{ - Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, - Point, Rectangle, Row, Text, VerticalAlignment, Widget, + Align, Clipboard, Color, Element, Hasher, HorizontalAlignment, Layout, + Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; /// A box that can be checked. @@ -39,6 +39,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> { spacing: u16, text_size: Option<u16>, font: Renderer::Font, + text_color: Option<Color>, style: Renderer::Style, } @@ -66,6 +67,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer> spacing: Renderer::DEFAULT_SPACING, text_size: None, font: Renderer::Font::default(), + text_color: None, style: Renderer::Style::default(), } } @@ -102,6 +104,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer> self } + /// Sets the text color of the [`Checkbox`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + /// Sets the style of the [`Checkbox`]. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); @@ -193,7 +201,7 @@ where &self.label, self.text_size.unwrap_or(renderer.default_size()), self.font, - None, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index d7f0365a..52a2e80c 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -14,7 +15,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -35,7 +36,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { ) -> Self { Column { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -55,9 +56,9 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the padding of the [`Column`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -129,7 +130,7 @@ where layout::flex::Axis::Vertical, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 69fe699b..69aee64d 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,7 +5,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::u32; @@ -15,7 +16,7 @@ use std::u32; /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] pub struct Container<'a, Message, Renderer: self::Renderer> { - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -36,7 +37,7 @@ where T: Into<Element<'a, Message, Renderer>>, { Container { - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -48,9 +49,9 @@ where } } - /// Sets the padding of the [`Container`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -127,23 +128,24 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits .loose() .max_width(self.max_width) .max_height(self.max_height) .width(self.width) .height(self.height) - .pad(padding); + .pad(self.padding); let mut content = self.content.layout(renderer, &limits.loose()); let size = limits.resolve(content.size()); - content.move_to(Point::new(padding, padding)); + content.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); content.align(self.horizontal_alignment, self.vertical_alignment, size); - layout::Node::with_children(size.pad(padding), vec![content]) + layout::Node::with_children(size.pad(self.padding), vec![content]) } fn on_event( diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index a006c0af..405daf00 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -132,19 +132,30 @@ where ) -> layout::Node { let (width, height) = renderer.dimensions(&self.handle); - let aspect_ratio = width as f32 / height as f32; - let mut size = limits .width(self.width) .height(self.height) .resolve(Size::new(width as f32, height as f32)); - let viewport_aspect_ratio = size.width / size.height; - - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; + let expansion_size = if height > width { + self.width } else { - size.height = height as f32 * size.width / width as f32; + self.height + }; + + // Only calculate viewport sizes if the images are constrained to a limited space. + // If they are Fill|Portion let them expand within their alotted space. + match expansion_size { + Length::Shrink | Length::Units(_) => { + let aspect_ratio = width as f32 / height as f32; + let viewport_aspect_ratio = size.width / size.height; + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } + } + Length::Fill | Length::FillPortion(_) => {} } layout::Node::new(size) diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 44028f5e..26a72409 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! 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 +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid mod axis; mod configuration; mod content; @@ -452,24 +452,25 @@ where _ => {} } - if self.state.picked_pane().is_none() { - self.elements - .iter_mut() - .zip(layout.children()) - .map(|((_, pane), layout)| { - pane.on_event( - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - messages, - ) - }) - .fold(event_status, event::Status::merge) - } else { - event::Status::Captured - } + let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + + self.elements + .iter_mut() + .zip(layout.children()) + .map(|((pane, content), layout)| { + let is_picked = picked_pane == Some(*pane); + + content.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + messages, + is_picked, + ) + }) + .fold(event_status, event::Status::merge) } fn draw( diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index f028ec25..bac9fdd4 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -149,6 +149,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, + is_picked: bool, ) -> event::Status { let mut event_status = event::Status::Ignored; @@ -169,14 +170,18 @@ where layout }; - let body_status = self.body.on_event( - event, - body_layout, - cursor_position, - renderer, - clipboard, - messages, - ); + let body_status = if is_picked { + event::Status::Ignored + } else { + self.body.on_event( + event, + body_layout, + cursor_position, + renderer, + clipboard, + messages, + ) + }; event_status.merge(body_status) } @@ -193,18 +198,17 @@ where &mut self, layout: Layout<'_>, ) -> Option<overlay::Element<'_, Message, Renderer>> { - let body_layout = if self.title_bar.is_some() { + if let Some(title_bar) = self.title_bar.as_mut() { let mut children = layout.children(); + let title_bar_layout = children.next()?; - // Overlays only allowed in the pane body, for now at least. - let _title_bar_layout = children.next(); - - children.next()? + match title_bar.overlay(title_bar_layout) { + Some(overlay) => Some(overlay), + None => self.body.overlay(children.next()?), + } } else { - layout - }; - - self.body.overlay(body_layout) + self.body.overlay(layout) + } } } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index a1e5107e..070010f8 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -1,8 +1,11 @@ use crate::container; use crate::event::{self, Event}; use crate::layout; +use crate::overlay; use crate::pane_grid; -use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; +use crate::{ + Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size, +}; /// The title bar of a [`Pane`]. /// @@ -11,7 +14,7 @@ use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size}; pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> { content: Element<'a, Message, Renderer>, controls: Option<Element<'a, Message, Renderer>>, - padding: u16, + padding: Padding, always_show_controls: bool, style: <Renderer as container::Renderer>::Style, } @@ -28,7 +31,7 @@ where Self { content: content.into(), controls: None, - padding: 0, + padding: Padding::ZERO, always_show_controls: false, style: Default::default(), } @@ -43,9 +46,9 @@ where self } - /// Sets the padding of the [`TitleBar`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TitleBar`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -129,15 +132,16 @@ where if layout.bounds().contains(cursor_position) { let mut children = layout.children(); let padded = children.next().unwrap(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); if self.controls.is_some() { - let mut children = padded.children(); - let _ = children.next().unwrap(); let controls_layout = children.next().unwrap(); !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) } else { - true + !title_layout.bounds().contains(cursor_position) } } else { false @@ -160,8 +164,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); - let limits = limits.pad(padding); + let limits = limits.pad(self.padding); let max_size = limits.max(); let title_layout = self @@ -191,9 +194,12 @@ where ) }; - node.move_to(Point::new(padding, padding)); + node.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(node.size().pad(padding), vec![node]) + layout::Node::with_children(node.size().pad(self.padding), vec![node]) } pub(crate) fn on_event( @@ -205,16 +211,17 @@ where clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, ) -> event::Status { - if let Some(controls) = &mut self.controls { - let mut children = layout.children(); - let padded = children.next().unwrap(); + let mut children = layout.children(); + let padded = children.next().unwrap(); - let mut children = padded.children(); - let _ = children.next(); + let mut children = padded.children(); + let title_layout = children.next().unwrap(); + + let control_status = if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); controls.on_event( - event, + event.clone(), controls_layout, cursor_position, renderer, @@ -223,6 +230,40 @@ where ) } else { event::Status::Ignored - } + }; + + let title_status = self.content.on_event( + event, + title_layout, + cursor_position, + renderer, + clipboard, + messages, + ); + + control_status.merge(title_status) + } + + pub(crate) fn overlay( + &mut self, + layout: Layout<'_>, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + let mut children = layout.children(); + let padded = children.next()?; + + let mut children = padded.children(); + let title_layout = children.next()?; + + let Self { + content, controls, .. + } = self; + + content.overlay(title_layout).or_else(move || { + controls.as_mut().and_then(|controls| { + let controls_layout = children.next()?; + + controls.overlay(controls_layout) + }) + }) } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 046d5779..d7792000 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -1,5 +1,6 @@ //! Display a dropdown list of selectable values. use crate::event::{self, Event}; +use crate::keyboard; use crate::layout; use crate::mouse; use crate::overlay; @@ -8,7 +9,8 @@ use crate::scrollable; use crate::text; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::borrow::Cow; @@ -19,14 +21,16 @@ where [T]: ToOwned<Owned = Vec<T>>, { menu: &'a mut menu::State, + keyboard_modifiers: &'a mut keyboard::Modifiers, 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]>, + placeholder: Option<String>, selected: Option<T>, width: Length, - padding: u16, + padding: Padding, text_size: Option<u16>, font: Renderer::Font, style: <Renderer as self::Renderer>::Style, @@ -36,6 +40,7 @@ where #[derive(Debug, Clone)] pub struct State<T> { menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, last_selection: Option<T>, @@ -45,6 +50,7 @@ impl<T> Default for State<T> { fn default() -> Self { Self { menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), last_selection: Option::default(), @@ -69,6 +75,7 @@ where ) -> Self { let State { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, @@ -76,11 +83,13 @@ where Self { menu, + keyboard_modifiers, is_open, hovered_option, last_selection, on_selected: Box::new(on_selected), options: options.into(), + placeholder: None, selected, width: Length::Shrink, text_size: None, @@ -90,15 +99,21 @@ where } } + /// Sets the placeholder of the [`PickList`]. + pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self { + self.placeholder = Some(placeholder.into()); + self + } + /// Sets the width of the [`PickList`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the padding of the [`PickList`]. - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`PickList`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -150,27 +165,37 @@ where let limits = limits .width(self.width) .height(Length::Shrink) - .pad(f32::from(self.padding)); + .pad(self.padding); let text_size = self.text_size.unwrap_or(renderer.default_size()); + let font = self.font; let max_width = match self.width { Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font, + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }; + 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) + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); + + let placeholder_width = self + .placeholder + .as_ref() + .map(String::as_str) + .map(measure) + .unwrap_or(100); + + labels_width.max(placeholder_width) } _ => 0, }; @@ -179,11 +204,11 @@ where let intrinsic = Size::new( max_width as f32 + f32::from(text_size) - + f32::from(self.padding), + + f32::from(self.padding.left), f32::from(text_size), ); - limits.resolve(intrinsic).pad(f32::from(self.padding)) + limits.resolve(intrinsic).pad(self.padding) }; layout::Node::new(size) @@ -194,6 +219,8 @@ where match self.width { Length::Shrink => { + self.placeholder.hash(state); + self.options .iter() .map(ToString::to_string) @@ -247,6 +274,48 @@ where event_status } } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) if self.keyboard_modifiers.command() + && layout.bounds().contains(cursor_position) + && !*self.is_open => + { + fn find_next<'a, T: PartialEq>( + selected: &'a T, + mut options: impl Iterator<Item = &'a T>, + ) -> Option<&'a T> { + let _ = options.find(|&option| option == selected); + + options.next() + } + + let next_option = if y < 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter()) + } else { + self.options.first() + } + } else if y > 0.0 { + if let Some(selected) = self.selected.as_ref() { + find_next(selected, self.options.iter().rev()) + } else { + self.options.last() + } + } else { + None + }; + + if let Some(next_option) = next_option { + messages.push((self.on_selected)(next_option.clone())); + } + + event::Status::Captured + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + *self.keyboard_modifiers = modifiers; + + event::Status::Ignored + } _ => event::Status::Ignored, } } @@ -264,6 +333,7 @@ where layout.bounds(), cursor_position, self.selected.as_ref().map(ToString::to_string), + self.placeholder.as_ref().map(String::as_str), self.padding, self.text_size.unwrap_or(renderer.default_size()), self.font, @@ -308,7 +378,7 @@ where /// [renderer]: crate::renderer pub trait Renderer: text::Renderer + menu::Renderer { /// The default padding of a [`PickList`]. - const DEFAULT_PADDING: u16; + const DEFAULT_PADDING: Padding; /// The [`PickList`] style supported by this renderer. type Style: Default; @@ -324,7 +394,8 @@ pub trait Renderer: text::Renderer + menu::Renderer { bounds: Rectangle, cursor_position: Point, selected: Option<String>, - padding: u16, + placeholder: Option<&str>, + padding: Padding, text_size: u16, font: Self::Font, style: &<Self as Renderer>::Style, diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 9482a9b1..dee82d1f 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,17 +1,17 @@ //! Create choices using radio buttons. +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, Color}; use crate::{ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; -use std::hash::Hash; - /// A circular button representing a choice. /// /// # Example @@ -47,6 +47,8 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> { size: u16, spacing: u16, text_size: Option<u16>, + text_color: Option<Color>, + font: Renderer::Font, style: Renderer::Style, } @@ -81,6 +83,8 @@ where size: <Renderer as self::Renderer>::DEFAULT_SIZE, spacing: Renderer::DEFAULT_SPACING, //15 text_size: None, + text_color: None, + font: Default::default(), style: Renderer::Style::default(), } } @@ -109,6 +113,18 @@ where self } + /// Sets the text color of the [`Radio`] button. + pub fn text_color(mut self, color: Color) -> Self { + self.text_color = Some(color); + self + } + + /// Sets the text font of the [`Radio`] button. + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + /// Sets the style of the [`Radio`] button. pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); @@ -196,8 +212,8 @@ where label_layout.bounds(), &self.label, self.text_size.unwrap_or(renderer.default_size()), - Default::default(), - None, + self.font, + self.text_color, HorizontalAlignment::Left, VerticalAlignment::Center, ); diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 5634ab12..9ebc9145 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -3,7 +3,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::overlay; use crate::{ - Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget, + Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point, + Rectangle, Widget, }; use std::hash::Hash; @@ -13,7 +14,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -34,7 +35,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { ) -> Self { Row { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -54,9 +55,9 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the padding of the [`Row`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -128,7 +129,7 @@ where layout::flex::Axis::Horizontal, renderer, &limits, - self.padding as f32, + self.padding, self.spacing as f32, self.align_items, &self.children, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 70ebebe2..68da2e67 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -6,7 +6,7 @@ use crate::mouse; use crate::overlay; use crate::touch; use crate::{ - Align, Clipboard, Column, Element, Hasher, Layout, Length, Point, + Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point, Rectangle, Size, Vector, Widget, }; @@ -23,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> { scrollbar_margin: u16, scroller_width: u16, content: Column<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(f32) -> Message>>, style: Renderer::Style, } @@ -37,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { scrollbar_margin: 0, scroller_width: 10, content: Column::new(), + on_scroll: None, style: Renderer::Style::default(), } } @@ -51,9 +53,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self } - /// Sets the padding of the [`Scrollable`]. - pub fn padding(mut self, units: u16) -> Self { - self.content = self.content.padding(units); + /// Sets the [`Padding`] of the [`Scrollable`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.content = self.content.padding(padding); self } @@ -101,12 +103,22 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { } /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. + /// + /// It silently enforces a minimum value of 1. pub fn scroller_width(mut self, scroller_width: u16) -> Self { self.scroller_width = scroller_width.max(1); self } + /// Sets a function to call when the [`Scrollable`] is scrolled. + /// + /// The function takes the new relative offset of the [`Scrollable`] + /// (e.g. `0` means top, while `1` means bottom). + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + self.on_scroll = Some(Box::new(f)); + self + } + /// Sets the style of the [`Scrollable`] . pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { self.style = style.into(); @@ -121,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } + + fn notify_on_scroll( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + messages: &mut Vec<Message>, + ) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = &self.on_scroll { + messages.push(on_scroll( + self.state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } + } } impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -228,6 +258,8 @@ where } } + self.notify_on_scroll(bounds, content_bounds, messages); + return event::Status::Captured; } Event::Touch(event) => { @@ -251,6 +283,12 @@ where self.state.scroll_box_touched_at = Some(cursor_position); + + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); } } touch::Event::FingerLifted { .. } @@ -290,6 +328,8 @@ where content_bounds, ); + self.notify_on_scroll(bounds, content_bounds, messages); + return event::Status::Captured; } } @@ -317,6 +357,12 @@ where self.state.scroller_grabbed_at = Some(scroller_grabbed_at); + self.notify_on_scroll( + bounds, + content_bounds, + messages, + ); + return event::Status::Captured; } } @@ -418,11 +464,44 @@ where } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { scroller_grabbed_at: Option<f32>, scroll_box_touched_at: Option<Point>, - offset: f32, + offset: Offset, +} + +impl Default for State { + fn default() -> Self { + Self { + scroller_grabbed_at: None, + scroll_box_touched_at: None, + offset: Offset::Absolute(0.0), + } + } +} + +/// The local state of a [`Scrollable`]. +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + match self { + Self::Absolute(absolute) => { + let hidden_content = + (content_bounds.height - bounds.height).max(0.0); + + absolute.min(hidden_content) + } + Self::Relative(percentage) => { + ((content_bounds.height - bounds.height) * percentage).max(0.0) + } + } + } } impl State { @@ -443,13 +522,14 @@ impl State { return; } - self.offset = (self.offset - delta_y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32); + self.offset = Offset::Absolute( + (self.offset.absolute(bounds, content_bounds) - delta_y) + .max(0.0) + .min((content_bounds.height - bounds.height) as f32), + ); } - /// Moves the scroll position to a relative amount, given the bounds of - /// the [`Scrollable`] and its contents. + /// Scrolls the [`Scrollable`] to a relative amount. /// /// `0` represents scrollbar at the top, while `1` represents scrollbar at /// the bottom. @@ -459,17 +539,29 @@ impl State { bounds: Rectangle, content_bounds: Rectangle, ) { + self.snap_to(percentage); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a relative amount. + /// + /// `0` represents scrollbar at the top, while `1` represents scrollbar at + /// the bottom. + pub fn snap_to(&mut self, percentage: f32) { + self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); + } + + /// Unsnaps the current scroll position, if snapped, given the bounds of the + /// [`Scrollable`] and its contents. + pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { self.offset = - ((content_bounds.height - bounds.height) * percentage).max(0.0); + Offset::Absolute(self.offset.absolute(bounds, content_bounds)); } /// Returns the current scrolling offset of the [`State`], given the bounds /// of the [`Scrollable`] and its contents. pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0).round() as u32; - - self.offset.min(hidden_content as f32) as u32 + self.offset.absolute(bounds, content_bounds) as u32 } /// Returns whether the scroller is currently grabbed or not. diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 6cc18e6c..adf6a74f 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -4,6 +4,8 @@ use crate::{ Rectangle, Size, VerticalAlignment, Widget, }; +pub use iced_core::text::Hit; + use std::hash::Hash; /// A paragraph of text. @@ -179,6 +181,23 @@ pub trait Renderer: crate::Renderer { bounds: Size, ) -> (f32, f32); + /// Tests whether the provided point is within the boundaries of [`Text`] + /// laid out with the given parameters, returning information about + /// the nearest character. + /// + /// If `nearest_only` is true, the hit test does not consider whether the + /// the point is interior to any glyph bounds, returning only the character + /// with the nearest centeroid. + fn hit_test( + &self, + contents: &str, + size: f32, + font: Self::Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> Hit; + /// Draws a [`Text`] fragment. /// /// It receives: diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index de6032b7..f1a7a1a0 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -18,7 +18,8 @@ use crate::mouse::{self, click}; use crate::text; use crate::touch; use crate::{ - Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, + Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -56,7 +57,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> { font: Renderer::Font, width: Length, max_width: u32, - padding: u16, + padding: Padding, size: Option<u16>, on_change: Box<dyn Fn(String) -> Message>, on_submit: Option<Message>, @@ -92,7 +93,7 @@ where font: Default::default(), width: Length::Fill, max_width: u32::MAX, - padding: 0, + padding: Padding::ZERO, size: None, on_change: Box::new(on_change), on_submit: None, @@ -126,9 +127,9 @@ where self } - /// Sets the padding of the [`TextInput`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -223,19 +224,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = self.padding as f32; let text_size = self.size.unwrap_or(renderer.default_size()); let limits = limits - .pad(padding) + .pad(self.padding) .width(self.width) .max_width(self.max_width) .height(Length::Units(text_size)); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new(padding, padding)); + text.move_to(Point::new( + self.padding.left.into(), + self.padding.top.into(), + )); - layout::Node::with_children(text.size().pad(padding), vec![text]) + layout::Node::with_children(text.size().pad(self.padding), vec![text]) } fn on_event( @@ -359,7 +362,7 @@ where Event::Keyboard(keyboard::Event::CharacterReceived(c)) if self.state.is_focused && self.state.is_pasting.is_none() - && !self.state.keyboard_modifiers.is_command_pressed() + && !self.state.keyboard_modifiers.command() && !c.is_control() => { let mut editor = @@ -447,7 +450,7 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - if modifiers.shift { + if modifiers.shift() { self.state .cursor .select_left_by_words(&self.value); @@ -456,7 +459,7 @@ where .cursor .move_left_by_words(&self.value); } - } else if modifiers.shift { + } else if modifiers.shift() { self.state.cursor.select_left(&self.value) } else { self.state.cursor.move_left(&self.value); @@ -466,7 +469,7 @@ where if platform::is_jump_modifier_pressed(modifiers) && !self.is_secure { - if modifiers.shift { + if modifiers.shift() { self.state .cursor .select_right_by_words(&self.value); @@ -475,14 +478,14 @@ where .cursor .move_right_by_words(&self.value); } - } else if modifiers.shift { + } 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 { + if modifiers.shift() { self.state.cursor.select_range( self.state.cursor.start(&self.value), 0, @@ -492,7 +495,7 @@ where } } keyboard::KeyCode::End => { - if modifiers.shift { + if modifiers.shift() { self.state.cursor.select_range( self.state.cursor.start(&self.value), self.value.len(), @@ -502,10 +505,7 @@ where } } keyboard::KeyCode::C - if self - .state - .keyboard_modifiers - .is_command_pressed() => + if self.state.keyboard_modifiers.command() => { match self.state.cursor.selection(&self.value) { Some((start, end)) => { @@ -517,10 +517,7 @@ where } } keyboard::KeyCode::X - if self - .state - .keyboard_modifiers - .is_command_pressed() => + if self.state.keyboard_modifiers.command() => { match self.state.cursor.selection(&self.value) { Some((start, end)) => { @@ -542,7 +539,7 @@ where messages.push(message); } keyboard::KeyCode::V => { - if self.state.keyboard_modifiers.is_command_pressed() { + if self.state.keyboard_modifiers.command() { let content = match self.state.is_pasting.take() { Some(content) => content, None => { @@ -573,10 +570,7 @@ where } } keyboard::KeyCode::A - if self - .state - .keyboard_modifiers - .is_command_pressed() => + if self.state.keyboard_modifiers.command() => { self.state.cursor.select_all(&self.value); } @@ -588,6 +582,11 @@ where self.state.keyboard_modifiers = keyboard::Modifiers::default(); } + keyboard::KeyCode::Tab + | keyboard::KeyCode::Up + | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } _ => {} } @@ -600,6 +599,11 @@ where keyboard::KeyCode::V => { self.state.is_pasting = None; } + keyboard::KeyCode::Tab + | keyboard::KeyCode::Up + | keyboard::KeyCode::Down => { + return event::Status::Ignored; + } _ => {} } @@ -703,15 +707,15 @@ pub trait Renderer: text::Renderer + Sized { let offset = self.offset(text_bounds, font, size, &value, &state); - find_cursor_position( - self, - &value, + self.hit_test( + &value.to_string(), + size.into(), font, - size, - x + offset, - 0, - value.len(), + Size::INFINITY, + Point::new(x + offset, text_bounds.height / 2.0), + true, ) + .cursor() } } @@ -792,61 +796,10 @@ impl State { 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: Renderer::Font, - size: u16, - target: f32, - start: usize, - end: usize, -) -> usize { - if start >= end { - if start == 0 { - return 0; - } - - let prev = value.until(start - 1); - let next = value.until(start); - - let prev_width = renderer.measure_value(&prev.to_string(), size, font); - let next_width = renderer.measure_value(&next.to_string(), size, font); - - if next_width - target > target - prev_width { - return start - 1; - } else { - return start; - } - } - - let index = (end - start) / 2; - let subvalue = value.until(start + index); - - let width = renderer.measure_value(&subvalue.to_string(), size, font); - if width > target { - find_cursor_position( - renderer, - value, - font, - size, - target, - start, - start + index, - ) - } else { - find_cursor_position( - renderer, - value, - font, - size, - target, - start + index + 1, - end, - ) + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + self.cursor.select_range(0, usize::MAX); } } @@ -855,9 +808,9 @@ mod platform { pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { if cfg!(target_os = "macos") { - modifiers.alt + modifiers.alt() } else { - modifiers.control + modifiers.control() } } } diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs index 1e7aee83..4f3b159b 100644 --- a/native/src/widget/text_input/cursor.rs +++ b/native/src/widget/text_input/cursor.rs @@ -113,7 +113,7 @@ impl Cursor { State::Selection { start, end } if end > 0 => { self.select_range(start, end - 1) } - _ => (), + _ => {} } } @@ -125,7 +125,7 @@ impl Cursor { State::Selection { start, end } if end < value.len() => { self.select_range(start, end + 1) } - _ => (), + _ => {} } } diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index 20e42567..0b50a382 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -20,7 +20,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert(self.cursor.end(self.value), character); @@ -35,7 +35,7 @@ impl<'a> Editor<'a> { self.cursor.move_left(self.value); self.value.remove_many(left, right); } - _ => (), + _ => {} } self.value.insert_many(self.cursor.end(self.value), content); diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs new file mode 100644 index 00000000..4035276c --- /dev/null +++ b/native/src/widget/toggler.rs @@ -0,0 +1,277 @@ +//! Show toggle controls using togglers. +use std::hash::Hash; + +use crate::{ + event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, + HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, + VerticalAlignment, Widget, +}; + +/// A toggler widget +/// +/// # Example +/// +/// ``` +/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>; +/// # +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); +/// ``` +#[allow(missing_debug_implementations)] +pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> { + is_active: bool, + on_toggle: Box<dyn Fn(bool) -> Message>, + label: Option<String>, + width: Length, + size: u16, + text_size: Option<u16>, + text_alignment: HorizontalAlignment, + spacing: u16, + font: Renderer::Font, + style: Renderer::Style, +} + +impl<Message, Renderer: self::Renderer + text::Renderer> + Toggler<Message, Renderer> +{ + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new<F>( + is_active: bool, + label: impl Into<Option<String>>, + f: F, + ) -> Self + where + F: 'static + Fn(bool) -> Message, + { + Toggler { + is_active, + on_toggle: Box::new(f), + label: label.into(), + width: Length::Fill, + size: <Renderer as self::Renderer>::DEFAULT_SIZE, + text_size: None, + text_alignment: HorizontalAlignment::Left, + spacing: 0, + font: Renderer::Font::default(), + style: Renderer::Style::default(), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: u16) -> Self { + self.size = size; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: u16) -> Self { + self.text_size = Some(text_size); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: HorizontalAlignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: u16) -> Self { + self.spacing = spacing; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + pub fn font(mut self, font: Renderer::Font) -> Self { + self.font = font; + self + } + + /// Sets the style of the [`Toggler`]. + pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self { + self.style = style.into(); + self + } +} + +impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer> +where + Renderer: self::Renderer + text::Renderer + row::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let mut row = Row::<(), Renderer>::new() + .width(self.width) + .spacing(self.spacing) + .align_items(Align::Center); + + if let Some(label) = &self.label { + row = row.push( + Text::new(label) + .horizontal_alignment(self.text_alignment) + .font(self.font) + .width(self.width) + .size(self.text_size.unwrap_or(renderer.default_size())), + ); + } + + row = row.push( + Row::new() + .width(Length::Units(2 * self.size)) + .height(Length::Units(self.size)), + ); + + row.layout(renderer, limits) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + messages: &mut Vec<Message>, + ) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = layout.bounds().contains(cursor_position); + + if mouse_over { + messages.push((self.on_toggle)(!self.is_active)); + + event::Status::Captured + } else { + event::Status::Ignored + } + } + _ => event::Status::Ignored, + } + } + + fn draw( + &self, + renderer: &mut Renderer, + defaults: &Renderer::Defaults, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) -> Renderer::Output { + let bounds = layout.bounds(); + let mut children = layout.children(); + + let label = match &self.label { + Some(label) => { + let label_layout = children.next().unwrap(); + + Some(text::Renderer::draw( + renderer, + defaults, + label_layout.bounds(), + &label, + self.text_size.unwrap_or(renderer.default_size()), + self.font, + None, + self.text_alignment, + VerticalAlignment::Center, + )) + } + + None => None, + }; + + let toggler_layout = children.next().unwrap(); + let toggler_bounds = toggler_layout.bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + self::Renderer::draw( + renderer, + toggler_bounds, + self.is_active, + is_mouse_over, + label, + &self.style, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + struct Marker; + std::any::TypeId::of::<Marker>().hash(state); + + self.label.hash(state) + } +} + +/// The renderer of a [`Toggler`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Toggler`] in your user interface. +/// +/// [renderer]: ../../renderer/index.html +pub trait Renderer: crate::Renderer { + /// The style supported by this renderer. + type Style: Default; + + /// The default size of a [`Toggler`]. + const DEFAULT_SIZE: u16; + + /// Draws a [`Toggler`]. + /// + /// It receives: + /// * the bounds of the [`Toggler`] + /// * whether the [`Toggler`] is activated or not + /// * whether the mouse is over the [`Toggler`] or not + /// * the drawn label of the [`Toggler`] + /// * the style of the [`Toggler`] + fn draw( + &mut self, + bounds: Rectangle, + is_active: bool, + is_mouse_over: bool, + label: Option<Self::Output>, + style: &Self::Style, + ) -> Self::Output; +} + +impl<'a, Message, Renderer> From<Toggler<Message, Renderer>> + for Element<'a, Message, Renderer> +where + Renderer: 'a + self::Renderer + text::Renderer + row::Renderer, + Message: 'a, +{ + fn from( + toggler: Toggler<Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(toggler) + } +} diff --git a/native/src/window/event.rs b/native/src/window/event.rs index fc746781..64f2b8d8 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -3,6 +3,14 @@ use std::path::PathBuf; /// A window-related event. #[derive(PartialEq, Clone, Debug)] pub enum Event { + /// A window was moved. + Moved { + /// The new logical x location of the window + x: i32, + /// The new logical y location of the window + y: i32, + }, + /// A window was resized. Resized { /// The new width of the window (in units) @@ -12,6 +20,12 @@ pub enum Event { height: u32, }, + /// The user has requested for the window to close. + /// + /// Usually, you will want to terminate the execution whenever this event + /// occurs. + CloseRequested, + /// A window was focused. Focused, diff --git a/rustfmt.toml b/rustfmt.toml index d979d317..fa50ab92 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ max_width=80 +edition="2018" diff --git a/src/application.rs b/src/application.rs index 4c6d5b7f..78280e98 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,6 +1,6 @@ use crate::window; use crate::{ - Clipboard, Color, Command, Element, Executor, Settings, Subscription, + Clipboard, Color, Command, Element, Executor, Menu, Settings, Subscription, }; /// An interactive cross-platform application. @@ -39,15 +39,15 @@ use crate::{ /// 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.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 +/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/0.3/examples/clock +/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.3/examples/download_progress +/// [`events`]: https://github.com/hecrj/iced/tree/0.3/examples/events +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/0.3/examples/game_of_life +/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.3/examples/pokedex +/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.3/examples/solar_system +/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.3/examples/stopwatch +/// [`todos`]: https://github.com/hecrj/iced/tree/0.3/examples/todos /// [`Sandbox`]: crate::Sandbox /// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ @@ -99,7 +99,7 @@ pub trait Application: Sized { type Executor: Executor; /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Clone + Send; /// The data needed to initialize your [`Application`]. type Flags; @@ -184,6 +184,20 @@ pub trait Application: Sized { 1.0 } + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// Returns the current system [`Menu`] of the [`Application`]. + /// + /// By default, it returns an empty [`Menu`]. + fn menu(&self) -> Menu<Self::Message> { + Menu::new() + } + /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread @@ -201,12 +215,13 @@ pub trait Application: Sized { let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, + text_multithreading: settings.text_multithreading, antialiasing: if settings.antialiasing { Some(crate::renderer::settings::Antialiasing::MSAAx4) } else { None }, - ..crate::renderer::Settings::default() + ..crate::renderer::Settings::from_env() }; Ok(crate::runtime::application::run::< @@ -270,6 +285,7 @@ where match self.0.mode() { window::Mode::Windowed => iced_winit::Mode::Windowed, window::Mode::Fullscreen => iced_winit::Mode::Fullscreen, + window::Mode::Hidden => iced_winit::Mode::Hidden, } } @@ -284,6 +300,14 @@ where fn scale_factor(&self) -> f64 { self.0.scale_factor() } + + fn should_exit(&self) -> bool { + self.0.should_exit() + } + + fn menu(&self) -> Menu<Self::Message> { + self.0.menu() + } } #[cfg(target_arch = "wasm32")] @@ -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.2/examples +//! [examples]: https://github.com/hecrj/iced/tree/0.3/examples //! [repository]: https://github.com/hecrj/iced //! //! # Overview @@ -245,7 +245,7 @@ pub use sandbox::Sandbox; pub use settings::Settings; pub use runtime::{ - futures, Align, Background, Clipboard, Color, Command, Font, - HorizontalAlignment, Length, Point, Rectangle, Size, Subscription, Vector, - VerticalAlignment, + futures, menu, Align, Background, Clipboard, Color, Command, Font, + HorizontalAlignment, Length, Menu, Point, Rectangle, Size, Subscription, + Vector, VerticalAlignment, }; diff --git a/src/sandbox.rs b/src/sandbox.rs index 9dd17b7e..cb3cf624 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -36,19 +36,19 @@ use crate::{ /// - [`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.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 +/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples +/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool +/// [`counter`]: https://github.com/hecrj/iced/tree/0.3/examples/counter +/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget +/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry +/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid +/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.3/examples/progress_bar +/// [`styling`]: https://github.com/hecrj/iced/tree/0.3/examples/styling +/// [`svg`]: https://github.com/hecrj/iced/tree/0.3/examples/svg +/// [`tour`]: https://github.com/hecrj/iced/tree/0.3/examples/tour /// [`Canvas widget`]: crate::widget::Canvas /// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu +/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu /// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// @@ -88,7 +88,7 @@ use crate::{ /// ``` pub trait Sandbox { /// The type of __messages__ your [`Sandbox`] will produce. - type Message: std::fmt::Debug + Send; + type Message: std::fmt::Debug + Clone + Send; /// Initializes the [`Sandbox`]. /// diff --git a/src/settings.rs b/src/settings.rs index c82a1354..d726dc4f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,12 +4,18 @@ use crate::window; /// The settings of an application. #[derive(Debug, Clone)] pub struct Settings<Flags> { + /// The identifier of the application. + /// + /// If provided, this identifier may be used to identify the application or + /// communicate with it through the windowing system. + pub id: Option<String>, + /// The window settings. /// /// They will be ignored on the Web. pub window: window::Settings, - /// The data needed to initialize an [`Application`]. + /// The data needed to initialize the [`Application`]. /// /// [`Application`]: crate::Application pub flags: Flags, @@ -25,6 +31,12 @@ pub struct Settings<Flags> { /// The default value is 20. pub default_text_size: u16, + /// If enabled, spread text workload in multiple threads when multiple cores + /// are available. + /// + /// By default, it is disabled. + pub text_multithreading: bool, + /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// @@ -35,6 +47,12 @@ pub struct Settings<Flags> { /// /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, + + /// Whether the [`Application`] should exit when the user requests the + /// window to close (e.g. the user presses the close button). + /// + /// By default, it is enabled. + pub exit_on_close_request: bool, } impl<Flags> Settings<Flags> { @@ -46,10 +64,13 @@ impl<Flags> Settings<Flags> { Self { flags, - antialiasing: default_settings.antialiasing, + id: default_settings.id, + window: default_settings.window, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, - window: default_settings.window, + text_multithreading: default_settings.text_multithreading, + antialiasing: default_settings.antialiasing, + exit_on_close_request: default_settings.exit_on_close_request, } } } @@ -60,11 +81,14 @@ where { fn default() -> Self { Self { + id: None, + window: Default::default(), flags: Default::default(), - antialiasing: Default::default(), default_font: Default::default(), default_text_size: 20, - window: Default::default(), + text_multithreading: false, + antialiasing: false, + exit_on_close_request: true, } } } @@ -73,8 +97,10 @@ where impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { + id: settings.id, window: settings.window.into(), flags: settings.flags, + exit_on_close_request: settings.exit_on_close_request, } } } diff --git a/src/widget.rs b/src/widget.rs index eac50d57..db052106 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -17,8 +17,8 @@ mod platform { pub use crate::renderer::widget::{ button, checkbox, container, pane_grid, pick_list, progress_bar, radio, - rule, scrollable, slider, text_input, tooltip, Column, Row, Space, - Text, + rule, scrollable, slider, text_input, toggler, tooltip, Column, Row, + Space, Text, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] @@ -53,7 +53,7 @@ mod platform { button::Button, checkbox::Checkbox, container::Container, image::Image, pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, - svg::Svg, text_input::TextInput, tooltip::Tooltip, + svg::Svg, text_input::TextInput, toggler::Toggler, tooltip::Tooltip, }; #[cfg(any(feature = "canvas", feature = "glow_canvas"))] diff --git a/src/window.rs b/src/window.rs index a2883b62..7d441062 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,9 +1,11 @@ //! Configure the window of your application in native platforms. mod mode; +mod position; mod settings; pub mod icon; pub use icon::Icon; pub use mode::Mode; +pub use position::Position; pub use settings::Settings; diff --git a/src/window/icon.rs b/src/window/icon.rs index 0d27b00e..287538b1 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -97,23 +97,25 @@ 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 \ + 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, - ) + byte_count, + ) } Error::DimensionsMismatch { width, height, pixel_count, } => { - write!(f, - "The number of RGBA pixels ({:?}) does not match the provided \ + write!( + f, + "The number of RGBA pixels ({:?}) does not match the provided \ dimensions ({:?}x{:?}).", - pixel_count, width, height, - ) + pixel_count, width, height, + ) } Error::OsError(e) => write!( f, diff --git a/src/window/mode.rs b/src/window/mode.rs index 37464711..fdce8e23 100644 --- a/src/window/mode.rs +++ b/src/window/mode.rs @@ -6,4 +6,7 @@ pub enum Mode { /// The application takes the whole screen of its current monitor. Fullscreen, + + /// The application is hidden + Hidden, } diff --git a/src/window/position.rs b/src/window/position.rs new file mode 100644 index 00000000..8535ef6a --- /dev/null +++ b/src/window/position.rs @@ -0,0 +1,33 @@ +/// The position of a window in a given screen. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The platform-specific default position for a new window. + Default, + /// The window is completely centered on the screen. + Centered, + /// The window is positioned with specific coordinates: `(X, Y)`. + /// + /// When the decorations of the window are enabled, Windows 10 will add some + /// invisible padding to the window. This padding gets included in the + /// position. So if you have decorations enabled and want the window to be + /// at (0, 0) you would have to set the position to + /// `(PADDING_X, PADDING_Y)`. + Specific(i32, i32), +} + +impl Default for Position { + fn default() -> Self { + Self::Default + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From<Position> for iced_winit::Position { + fn from(position: Position) -> Self { + match position { + Position::Default => Self::Default, + Position::Centered => Self::Centered, + Position::Specific(x, y) => Self::Specific(x, y), + } + } +} diff --git a/src/window/settings.rs b/src/window/settings.rs index 6b5d2985..ec6c3071 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -1,4 +1,4 @@ -use crate::window::Icon; +use crate::window::{Icon, Position}; /// The window settings of an application. #[derive(Debug, Clone)] @@ -6,6 +6,9 @@ pub struct Settings { /// The initial size of the window. pub size: (u32, u32), + /// The initial position of the window. + pub position: Position, + /// The minimum size of the window. pub min_size: Option<(u32, u32)>, @@ -32,6 +35,7 @@ impl Default for Settings { fn default() -> Settings { Settings { size: (1024, 768), + position: Position::default(), min_size: None, max_size: None, resizable: true, @@ -48,6 +52,7 @@ impl From<Settings> for iced_winit::settings::Window { fn from(settings: Settings) -> Self { Self { size: settings.size, + position: iced_winit::Position::from(settings.position), min_size: settings.min_size, max_size: settings.max_size, resizable: settings.resizable, diff --git a/style/Cargo.toml b/style/Cargo.toml index ac16f8ee..a3086477 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_style" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "The default set of styles of Iced" @@ -10,5 +10,6 @@ documentation = "https://docs.rs/iced_style" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] -[dependencies] -iced_core = { version = "0.3", path = "../core" } +[dependencies.iced_core] +version = "0.4" +path = "../core" diff --git a/style/src/lib.rs b/style/src/lib.rs index f09b5f9d..08d9f044 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -18,3 +18,4 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs index a757ba98..d1801e5f 100644 --- a/style/src/pick_list.rs +++ b/style/src/pick_list.rs @@ -5,6 +5,7 @@ use iced_core::{Background, Color}; #[derive(Debug, Clone, Copy)] pub struct Style { pub text_color: Color, + pub placeholder_color: Color, pub background: Background, pub border_radius: f32, pub border_width: f32, @@ -16,6 +17,7 @@ impl std::default::Default for Style { fn default() -> Self { Self { text_color: Color::BLACK, + placeholder_color: [0.4, 0.4, 0.4].into(), background: Background::Color([0.87, 0.87, 0.87].into()), border_radius: 0.0, border_width: 1.0, diff --git a/style/src/rule.rs b/style/src/rule.rs index 5021340b..221f48fb 100644 --- a/style/src/rule.rs +++ b/style/src/rule.rs @@ -79,6 +79,17 @@ pub struct Style { pub fill_mode: FillMode, } +impl std::default::Default for Style { + fn default() -> Self { + Style { + color: [0.6, 0.6, 0.6, 0.6].into(), + width: 1, + radius: 0.0, + fill_mode: FillMode::Full, + } + } +} + /// A set of rules that dictate the style of a rule. pub trait StyleSheet { /// Produces the style of a rule. @@ -89,12 +100,7 @@ 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), - } + Style::default() } } diff --git a/style/src/toggler.rs b/style/src/toggler.rs new file mode 100644 index 00000000..5a155123 --- /dev/null +++ b/style/src/toggler.rs @@ -0,0 +1,57 @@ +//! Show toggle controls using togglers. +use iced_core::Color; + +/// The appearance of a toggler. +#[derive(Debug)] +pub struct Style { + pub background: Color, + pub background_border: Option<Color>, + pub foreground: Color, + pub foreground_border: Option<Color>, +} + +/// A set of rules that dictate the style of a toggler. +pub trait StyleSheet { + fn active(&self, is_active: bool) -> Style; + + fn hovered(&self, is_active: bool) -> Style; +} + +struct Default; + +impl StyleSheet for Default { + fn active(&self, is_active: bool) -> Style { + Style { + background: if is_active { + Color::from_rgb(0.0, 1.0, 0.0) + } else { + Color::from_rgb(0.7, 0.7, 0.7) + }, + background_border: None, + foreground: Color::WHITE, + foreground_border: None, + } + } + + fn hovered(&self, is_active: bool) -> Style { + Style { + foreground: Color::from_rgb(0.95, 0.95, 0.95), + ..self.active(is_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/web/Cargo.toml b/web/Cargo.toml index f8630780..8a5d6374 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_web" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A web backend for Iced" @@ -23,15 +23,15 @@ num-traits = "0.2" base64 = "0.13" [dependencies.iced_core] -version = "0.3" +version = "0.4" path = "../core" [dependencies.iced_futures] -version = "0.2" +version = "0.3" path = "../futures" [dependencies.iced_style] -version = "0.2" +version = "0.3" path = "../style" [dependencies.web-sys] diff --git a/web/README.md b/web/README.md index 0e770589..58ad8235 100644 --- a/web/README.md +++ b/web/README.md @@ -16,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.3" +iced_web = "0.4" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/web/src/css.rs b/web/src/css.rs index bdde23f3..21f51f85 100644 --- a/web/src/css.rs +++ b/web/src/css.rs @@ -1,5 +1,5 @@ //! Style your widgets. -use crate::{bumpalo, Align, Background, Color, Length}; +use crate::{bumpalo, Align, Background, Color, Length, Padding}; use std::collections::BTreeMap; @@ -12,11 +12,11 @@ pub enum Rule { /// Container with horizonal distribution Row, - /// Padding of the container - Padding(u16), - /// Spacing between elements Spacing(u16), + + /// Toggler input for a specific size + Toggler(u16), } impl Rule { @@ -25,8 +25,8 @@ impl Rule { match self { Rule::Column => String::from("c"), Rule::Row => String::from("r"), - Rule::Padding(padding) => format!("p-{}", padding), Rule::Spacing(spacing) => format!("s-{}", spacing), + Rule::Toggler(size) => format!("toggler-{}", size), } } @@ -45,13 +45,6 @@ impl Rule { bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str() } - Rule::Padding(padding) => bumpalo::format!( - in bump, - ".{} {{ box-sizing: border-box; padding: {}px }}", - class, - padding - ) - .into_bump_str(), Rule::Spacing(spacing) => bumpalo::format!( in bump, ".c.{} > * {{ margin-bottom: {}px }} \ @@ -66,6 +59,46 @@ impl Rule { class ) .into_bump_str(), + Rule::Toggler(size) => bumpalo::format!( + in bump, + ".toggler-{} {{ display: flex; cursor: pointer; justify-content: space-between; }} \ + .toggler-{} input {{ display:none; }} \ + .toggler-{} span {{ background-color: #b1b1b1; position: relative; display: inline-flex; width:{}px; height: {}px; border-radius: {}px;}} \ + .toggler-{} span > span {{ background-color: #FFFFFF; width: {}px; height: {}px; border-radius: 50%; top: 1px; left: 1px;}} \ + .toggler-{}:hover span > span {{ background-color: #f1f1f1 !important; }} \ + .toggler-{} input:checked + span {{ background-color: #00FF00; }} \ + .toggler-{} input:checked + span > span {{ -webkit-transform: translateX({}px); -ms-transform:translateX({}px); transform: translateX({}px); }} + ", + // toggler + size, + + // toggler input + size, + + // toggler span + size, + size*2, + size, + size, + + // toggler span > span + size, + size-2, + size-2, + + // toggler: hover + span > span + size, + + // toggler input:checked + span + size, + + // toggler input:checked + span > span + size, + size, + size, + size + ) + .into_bump_str(), } } } @@ -170,3 +203,13 @@ pub fn align(align: Align) -> &'static str { Align::End => "flex-end", } } + +/// Returns the style value for the given [`Padding`]. +/// +/// [`Padding`]: struct.Padding.html +pub fn padding(padding: Padding) -> String { + format!( + "{}px {}px {}px {}px", + padding.top, padding.right, padding.bottom, padding.left + ) +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 4c65dfa3..bb09bb0d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -49,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.2/examples/tour +//! [`tour` example]: https://github.com/hecrj/iced/tree/0.3/examples/tour #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] @@ -74,8 +74,8 @@ pub use dodrio; pub use element::Element; pub use hasher::Hasher; pub use iced_core::{ - keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment, - Length, Point, Rectangle, Size, Vector, VerticalAlignment, + keyboard, menu, mouse, Align, Background, Color, Font, HorizontalAlignment, + Length, Menu, Padding, Point, Rectangle, Size, Vector, VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; pub use subscription::Subscription; @@ -244,3 +244,168 @@ where .finish() } } + +/// An interactive embedded web 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 either +/// take control of the `<body>' or of an HTML element of the document specified +/// by `container_id`. +/// +/// An [`Embedded`](trait.Embedded.html) can execute asynchronous actions +/// by returning a [`Command`](struct.Command.html) in some of its methods. +pub trait Embedded { + /// 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 [`Embedded`] application will produce. + /// + /// [`Embedded`]: trait.Embedded.html + type Message: Send; + + /// The data needed to initialize your [`Embedded`] application. + /// + /// [`Embedded`]: trait.Embedded.html + type Flags; + + /// Initializes the [`Embedded`] 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. + /// + /// [`Embedded`]: trait.Embedded.html + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) + where + Self: Sized; + + /// Handles a __message__ and updates the state of the [`Embedded`] + /// 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. + /// + /// [`Embedded`]: trait.Embedded.html + /// [`Command`]: struct.Command.html + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the [`Embedded`] application. + /// + /// These widgets can produce __messages__ based on user interaction. + /// + /// [`Embedded`]: trait.Embedded.html + fn view(&mut self) -> Element<'_, Self::Message>; + + /// Returns the event [`Subscription`] for the current state of the embedded + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + /// + /// [`Subscription`]: struct.Subscription.html + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Runs the [`Embedded`] application. + /// + /// [`Embedded`]: trait.Embedded.html + fn run(flags: Self::Flags, container_id: Option<String>) + where + Self: 'static + Sized, + { + use futures::stream::StreamExt; + use wasm_bindgen::JsCast; + use web_sys::HtmlElement; + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let container: HtmlElement = container_id + .map(|id| document.get_element_by_id(&id).unwrap()) + .map(|container| { + container.dyn_ref::<HtmlElement>().unwrap().to_owned() + }) + .unwrap_or_else(|| document.body().unwrap()); + + let (sender, receiver) = + iced_futures::futures::channel::mpsc::unbounded(); + + let mut runtime = iced_futures::Runtime::new( + Self::Executor::new().expect("Create executor"), + sender.clone(), + ); + + let (app, command) = runtime.enter(|| Self::new(flags)); + + runtime.spawn(command); + + let application = Rc::new(RefCell::new(app)); + + let instance = EmbeddedInstance { + application: application.clone(), + bus: Bus::new(sender), + }; + + let vdom = dodrio::Vdom::new(&container, instance); + + let event_loop = receiver.for_each(move |message| { + let (command, subscription) = runtime.enter(|| { + let command = application.borrow_mut().update(message); + let subscription = application.borrow().subscription(); + + (command, subscription) + }); + + runtime.spawn(command); + runtime.track(subscription); + + vdom.weak().schedule_render(); + + futures::future::ready(()) + }); + + wasm_bindgen_futures::spawn_local(event_loop); + } +} + +struct EmbeddedInstance<A: Embedded> { + application: Rc<RefCell<A>>, + bus: Bus<A::Message>, +} + +impl<'a, A> dodrio::Render<'a> for EmbeddedInstance<A> +where + A: Embedded, +{ + 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(context.bump, &self.bus, &mut css); + + div(context.bump) + .attr("style", "width: 100%; height: 100%") + .children(vec![css.node(context.bump), node]) + .finish() + } +} diff --git a/web/src/widget.rs b/web/src/widget.rs index 023f5f13..4cb0a9cc 100644 --- a/web/src/widget.rs +++ b/web/src/widget.rs @@ -24,6 +24,7 @@ pub mod radio; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; mod column; mod row; @@ -40,6 +41,8 @@ pub use slider::Slider; pub use text::Text; #[doc(no_inline)] pub use text_input::TextInput; +#[doc(no_inline)] +pub use toggler::Toggler; pub use checkbox::Checkbox; pub use column::Column; diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs index 7c389a9f..cd450b55 100644 --- a/web/src/widget/button.rs +++ b/web/src/widget/button.rs @@ -1,7 +1,7 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -use crate::{css, Background, Bus, Css, Element, Length, Widget}; +use crate::{css, Background, Bus, Css, Element, Length, Padding, Widget}; pub use iced_style::button::{Style, StyleSheet}; @@ -20,6 +20,26 @@ use dodrio::bumpalo; /// let button = Button::new(&mut state, Text::new("Press me!")) /// .on_press(Message::ButtonPressed); /// ``` +/// +/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will +/// be disabled: +/// +/// ``` +/// # use iced_web::{button, Button, Text}; +/// # +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { +/// Button::new(state, Text::new("I'm disabled!")) +/// } +/// +/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { +/// disabled_button(state).on_press(Message::ButtonPressed) +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Button<'a, Message> { content: Element<'a, Message>, @@ -30,7 +50,7 @@ pub struct Button<'a, Message> { min_width: u32, #[allow(dead_code)] min_height: u32, - padding: u16, + padding: Padding, style: Box<dyn StyleSheet>, } @@ -48,7 +68,7 @@ impl<'a, Message> Button<'a, Message> { height: Length::Shrink, min_width: 0, min_height: 0, - padding: 5, + padding: Padding::new(5), style: Default::default(), } } @@ -77,9 +97,9 @@ impl<'a, Message> Button<'a, Message> { self } - /// Sets the padding of the [`Button`]. - pub fn padding(mut self, padding: u16) -> Self { - self.padding = padding; + /// Sets the [`Padding`] of the [`Button`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -90,6 +110,7 @@ impl<'a, Message> Button<'a, Message> { } /// Sets the message that will be produced when the [`Button`] is pressed. + /// If on_press isn't set, button will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self @@ -122,9 +143,6 @@ where // TODO: State-based styling let style = self.style.active(); - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); - let background = match style.background { None => String::from("none"), Some(background) => match background { @@ -132,25 +150,19 @@ 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", class) .attr( "style", bumpalo::format!( in bump, "background: {}; border-radius: {}px; width:{}; \ - min-width: {}; color: {}", + min-width: {}; color: {}; padding: {}", background, style.border_radius, css::length(self.width), css::min_length(self.min_width), - css::color(style.text_color) + css::color(style.text_color), + css::padding(self.padding) ) .into_bump_str(), ) @@ -162,6 +174,8 @@ where node = node.on("click", move |_root, _vdom, _event| { event_bus.publish(on_press.clone()); }); + } else { + node = node.attr("disabled", ""); } node.finish() diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs index d832fdcb..8738c2af 100644 --- a/web/src/widget/column.rs +++ b/web/src/widget/column.rs @@ -1,4 +1,4 @@ -use crate::{css, Align, Bus, Css, Element, Length, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Padding, Widget}; use dodrio::bumpalo; use std::u32; @@ -9,7 +9,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Column<'a, Message> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -28,7 +28,7 @@ impl<'a, Message> Column<'a, Message> { pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { Column { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Fill, height: Length::Shrink, max_width: u32::MAX, @@ -48,9 +48,9 @@ impl<'a, Message> Column<'a, Message> { self } - /// Sets the padding of the [`Column`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -114,23 +114,21 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> { let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); - // TODO: Complete styling div(bump) .attr( "class", - bumpalo::format!(in bump, "{} {} {}", column_class, spacing_class, padding_class) + bumpalo::format!(in bump, "{} {}", column_class, spacing_class) .into_bump_str(), ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}", css::length(self.width), css::length(self.height), css::max_length(self.max_width), css::max_length(self.max_height), + css::padding(self.padding), css::align(self.align_items) ).into_bump_str() ) diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs index 8de3cc41..c006e011 100644 --- a/web/src/widget/container.rs +++ b/web/src/widget/container.rs @@ -1,5 +1,5 @@ //! Decorate content and apply alignment. -use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Widget}; +use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Padding, Widget}; pub use iced_style::container::{Style, StyleSheet}; @@ -8,7 +8,7 @@ pub use iced_style::container::{Style, StyleSheet}; /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] pub struct Container<'a, Message> { - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -29,7 +29,7 @@ impl<'a, Message> Container<'a, Message> { use std::u32; Container { - padding: 0, + padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, @@ -41,9 +41,9 @@ impl<'a, Message> Container<'a, Message> { } } - /// Sets the padding of the [`Container`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Container`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -106,24 +106,22 @@ where let column_class = style_sheet.insert(bump, css::Rule::Column); - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); - let style = self.style_sheet.style(); let node = div(bump) .attr( "class", - bumpalo::format!(in bump, "{} {}", column_class, padding_class).into_bump_str(), + bumpalo::format!(in bump, "{}", column_class).into_bump_str(), ) .attr( "style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px", + "width: {}; height: {}; max-width: {}; padding: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px", css::length(self.width), css::length(self.height), css::max_length(self.max_width), + css::padding(self.padding), css::align(self.horizontal_alignment), css::align(self.vertical_alignment), style.background.map(css::background).unwrap_or(String::from("initial")), diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs index f00a544a..ffb515cf 100644 --- a/web/src/widget/row.rs +++ b/web/src/widget/row.rs @@ -1,4 +1,4 @@ -use crate::{css, Align, Bus, Css, Element, Length, Widget}; +use crate::{css, Align, Bus, Css, Element, Length, Padding, Widget}; use dodrio::bumpalo; use std::u32; @@ -9,7 +9,7 @@ use std::u32; #[allow(missing_debug_implementations)] pub struct Row<'a, Message> { spacing: u16, - padding: u16, + padding: Padding, width: Length, height: Length, max_width: u32, @@ -28,7 +28,7 @@ impl<'a, Message> Row<'a, Message> { pub fn with_children(children: Vec<Element<'a, Message>>) -> Self { Row { spacing: 0, - padding: 0, + padding: Padding::ZERO, width: Length::Fill, height: Length::Shrink, max_width: u32::MAX, @@ -48,9 +48,9 @@ impl<'a, Message> Row<'a, Message> { self } - /// Sets the padding of the [`Row`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -114,23 +114,21 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> { let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(self.spacing)); - let padding_class = - style_sheet.insert(bump, css::Rule::Padding(self.padding)); - // TODO: Complete styling div(bump) .attr( "class", - bumpalo::format!(in bump, "{} {} {}", row_class, spacing_class, padding_class) + bumpalo::format!(in bump, "{} {}", row_class, spacing_class) .into_bump_str(), ) .attr("style", bumpalo::format!( in bump, - "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}", + "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}", css::length(self.width), css::length(self.height), css::max_length(self.max_width), css::max_length(self.max_height), + css::padding(self.padding), css::align(self.align_items) ).into_bump_str() ) diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs index bd9260ff..ce0a10d4 100644 --- a/web/src/widget/scrollable.rs +++ b/web/src/widget/scrollable.rs @@ -1,5 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. -use crate::{bumpalo, css, Align, Bus, Column, Css, Element, Length, Widget}; +use crate::{ + bumpalo, css, Align, Bus, Column, Css, Element, Length, Padding, Widget, +}; pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; @@ -39,9 +41,9 @@ impl<'a, Message> Scrollable<'a, Message> { self } - /// Sets the padding of the [`Scrollable`]. - pub fn padding(mut self, units: u16) -> Self { - self.content = self.content.padding(units); + /// Sets the [`Padding`] of the [`Scrollable`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.content = self.content.padding(padding); self } diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs index bc2048a8..e4877f2a 100644 --- a/web/src/widget/text_input.rs +++ b/web/src/widget/text_input.rs @@ -1,7 +1,7 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. -use crate::{bumpalo, css, Bus, Css, Element, Length, Widget}; +use crate::{bumpalo, css, Bus, Css, Element, Length, Padding, Widget}; pub use iced_style::text_input::{Style, StyleSheet}; @@ -35,7 +35,7 @@ pub struct TextInput<'a, Message> { is_secure: bool, width: Length, max_width: u32, - padding: u16, + padding: Padding, size: Option<u16>, on_change: Rc<Box<dyn Fn(String) -> Message>>, on_submit: Option<Message>, @@ -66,7 +66,7 @@ impl<'a, Message> TextInput<'a, Message> { is_secure: false, width: Length::Fill, max_width: u32::MAX, - padding: 0, + padding: Padding::ZERO, size: None, on_change: Rc::new(Box::new(on_change)), on_submit: None, @@ -92,9 +92,9 @@ impl<'a, Message> TextInput<'a, Message> { self } - /// Sets the padding of the [`TextInput`]. - pub fn padding(mut self, units: u16) -> Self { - self.padding = units; + /// Sets the [`Padding`] of the [`TextInput`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); self } @@ -126,20 +126,11 @@ where &self, bump: &'b bumpalo::Bump, bus: &Bus<Message>, - style_sheet: &mut Css<'b>, + _style_sheet: &mut Css<'b>, ) -> dodrio::Node<'b> { use dodrio::builder::*; use wasm_bindgen::JsCast; - 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; @@ -159,16 +150,16 @@ where let style = self.style_sheet.active(); input(bump) - .attr("class", class) .attr( "style", bumpalo::format!( in bump, - "width: {}; max-width: {}; font-size: {}px; \ + "width: {}; max-width: {}; padding: {}; font-size: {}px; \ background: {}; border-width: {}px; border-color: {}; \ border-radius: {}px; color: {}", css::length(self.width), css::max_length(self.max_width), + css::padding(self.padding), self.size.unwrap_or(20), css::background(style.background), style.border_width, @@ -232,4 +223,9 @@ impl State { // TODO Self::default() } + + /// Selects all the content of the [`TextInput`]. + pub fn select_all(&mut self) { + // TODO + } } diff --git a/web/src/widget/toggler.rs b/web/src/widget/toggler.rs new file mode 100644 index 00000000..0a198079 --- /dev/null +++ b/web/src/widget/toggler.rs @@ -0,0 +1,171 @@ +//! Show toggle controls using togglers. +use crate::{css, Bus, Css, Element, Length, Widget}; + +pub use iced_style::toggler::{Style, StyleSheet}; + +use dodrio::bumpalo; +use std::rc::Rc; + +/// A toggler that can be toggled. +/// +/// # Example +/// +/// ``` +/// # use iced_web::Toggler; +/// +/// pub enum Message { +/// TogglerToggled(bool), +/// } +/// +/// let is_active = true; +/// +/// Toggler::new(is_active, String::from("Toggle me!"), Message::TogglerToggled); +/// ``` +/// +#[allow(missing_debug_implementations)] +pub struct Toggler<Message> { + is_active: bool, + on_toggle: Rc<dyn Fn(bool) -> Message>, + label: Option<String>, + id: Option<String>, + width: Length, + style: Box<dyn StyleSheet>, +} + +impl<Message> Toggler<Message> { + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is active or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn new<F>( + is_active: bool, + label: impl Into<Option<String>>, + f: F, + ) -> Self + where + F: 'static + Fn(bool) -> Message, + { + Toggler { + is_active, + on_toggle: Rc::new(f), + label: label.into(), + id: None, + width: Length::Shrink, + style: Default::default(), + } + } + + /// Sets the width of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the style of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self { + self.style = style.into(); + self + } + + /// Sets the id of the [`Toggler`]. + /// + /// [`Toggler`]: struct.Toggler.html + pub fn id(mut self, id: impl Into<String>) -> Self { + self.id = Some(id.into()); + self + } +} + +impl<Message> Widget<Message> for Toggler<Message> +where + Message: 'static, +{ + fn node<'b>( + &self, + bump: &'b bumpalo::Bump, + bus: &Bus<Message>, + style_sheet: &mut Css<'b>, + ) -> dodrio::Node<'b> { + use dodrio::builder::*; + use dodrio::bumpalo::collections::String; + + let toggler_label = &self + .label + .as_ref() + .map(|label| String::from_str_in(&label, bump).into_bump_str()); + + let event_bus = bus.clone(); + let on_toggle = self.on_toggle.clone(); + let is_active = self.is_active; + + let row_class = style_sheet.insert(bump, css::Rule::Row); + let toggler_class = style_sheet.insert(bump, css::Rule::Toggler(16)); + + 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 checkbox = input + .attr("type", "checkbox") + .bool_attr("checked", self.is_active) + .on("click", move |_root, vdom, _event| { + let msg = on_toggle(!is_active); + event_bus.publish(msg); + + vdom.schedule_render(); + }) + .finish(); + + let toggler = span(bump).children(vec![span(bump).finish()]).finish(); + + label + .attr( + "class", + bumpalo::format!(in bump, "{} {}", row_class, toggler_class) + .into_bump_str(), + ) + .attr( + "style", + bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width)) + .into_bump_str() + ) + .children( + if let Some(label) = toggler_label { + vec![ + text(label), + checkbox, + toggler, + ] + } else { + vec![ + checkbox, + toggler, + ] + } + ) + .finish() + } +} + +impl<'a, Message> From<Toggler<Message>> for Element<'a, Message> +where + Message: 'static, +{ + fn from(toggler: Toggler<Message>) -> Element<'a, Message> { + Element::new(toggler) + } +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index aa39f068..a904fe87 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_wgpu" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A wgpu renderer for Iced" @@ -24,10 +24,11 @@ farbfeld = ["image_rs/farbfeld"] canvas = ["iced_graphics/canvas"] qr_code = ["iced_graphics/qr_code"] default_system_font = ["iced_graphics/font-source"] +spirv = ["wgpu/spirv"] [dependencies] -wgpu = "0.7" -wgpu_glyph = "0.11" +wgpu = "0.10" +wgpu_glyph = "0.14" glyph_brush = "0.7" raw-window-handle = "0.3" log = "0.4" @@ -39,11 +40,11 @@ version = "1.4" features = ["derive"] [dependencies.iced_native] -version = "0.3" +version = "0.4" path = "../native" [dependencies.iced_graphics] -version = "0.1" +version = "0.2" path = "../graphics" features = ["font-fallback", "font-icons"] diff --git a/wgpu/README.md b/wgpu/README.md index e8cb0a43..a9d95ea7 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -29,7 +29,7 @@ Currently, `iced_wgpu` supports the following primitives: Add `iced_wgpu` as a dependency in your `Cargo.toml`: ```toml -iced_wgpu = "0.3" +iced_wgpu = "0.4" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 534c6cb7..b31bf92c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -2,6 +2,7 @@ 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; @@ -30,18 +31,24 @@ pub struct Backend { 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( + pub fn new( + device: &wgpu::Device, + settings: Settings, + format: wgpu::TextureFormat, + ) -> Self { + let text_pipeline = text::Pipeline::new( device, - settings.format, - settings.antialiasing, + format, + settings.default_font, + settings.text_multithreading, ); + let quad_pipeline = quad::Pipeline::new(device, format); + let triangle_pipeline = + triangle::Pipeline::new(device, format, settings.antialiasing); + #[cfg(any(feature = "image_rs", feature = "svg"))] - let image_pipeline = image::Pipeline::new(device, settings.format); + let image_pipeline = image::Pipeline::new(device, format); Self { quad_pipeline, @@ -268,6 +275,25 @@ impl backend::Text for Backend { ) -> (f32, f32) { self.text_pipeline.measure(contents, size, font, bounds) } + + fn hit_test( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + point: iced_native::Point, + nearest_only: bool, + ) -> text::Hit { + self.text_pipeline.hit_test( + contents, + size, + font, + bounds, + point, + nearest_only, + ) + } } #[cfg(feature = "image_rs")] diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 5511565e..a59dc04b 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -61,7 +61,7 @@ impl Pipeline { entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::VERTEX, + visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, @@ -73,7 +73,7 @@ impl Pipeline { }, wgpu::BindGroupLayoutEntry { binding: 1, - visibility: wgpu::ShaderStage::FRAGMENT, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler { comparison: false, filtering: true, @@ -86,7 +86,7 @@ impl Pipeline { 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, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -97,11 +97,13 @@ impl Pipeline { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &uniforms_buffer, - offset: 0, - size: None, - }, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &uniforms_buffer, + offset: 0, + size: None, + }, + ), }, wgpu::BindGroupEntry { binding: 1, @@ -115,7 +117,7 @@ impl Pipeline { label: Some("iced_wgpu::image texture atlas layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::FRAGMENT, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true, @@ -134,86 +136,67 @@ impl Pipeline { bind_group_layouts: &[&constant_layout, &texture_layout], }); - let vs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/image.vert.spv" - )); - - let fs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/image.frag.spv" - )); + let shader = + device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu::image::shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("shader/image.wgsl"), + )), + }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("iced_wgpu::image pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", + module: &shader, + entry_point: "vs_main", buffers: &[ wgpu::VertexBufferLayout { array_stride: mem::size_of::<Vertex>() as u64, - step_mode: wgpu::InputStepMode::Vertex, + step_mode: wgpu::VertexStepMode::Vertex, attributes: &[wgpu::VertexAttribute { shader_location: 0, - format: wgpu::VertexFormat::Float2, + format: wgpu::VertexFormat::Float32x2, offset: 0, }], }, wgpu::VertexBufferLayout { array_stride: mem::size_of::<Instance>() as u64, - step_mode: wgpu::InputStepMode::Instance, - attributes: &[ - wgpu::VertexAttribute { - shader_location: 1, - format: wgpu::VertexFormat::Float2, - offset: 0, - }, - wgpu::VertexAttribute { - shader_location: 2, - format: wgpu::VertexFormat::Float2, - offset: 4 * 2, - }, - wgpu::VertexAttribute { - shader_location: 3, - format: wgpu::VertexFormat::Float2, - offset: 4 * 4, - }, - wgpu::VertexAttribute { - shader_location: 4, - format: wgpu::VertexFormat::Float2, - offset: 4 * 6, - }, - wgpu::VertexAttribute { - shader_location: 5, - format: wgpu::VertexFormat::Uint, - offset: 4 * 8, - }, - ], + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + 1 => Float32x2, + 2 => Float32x2, + 3 => Float32x2, + 4 => Float32x2, + 5 => Sint32, + ), }, ], }, fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", + module: &shader, + entry_point: "fs_main", targets: &[wgpu::ColorTargetState { format, - color_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - write_mask: wgpu::ColorWrite::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, }], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Cw, - cull_mode: wgpu::CullMode::None, ..Default::default() }, depth_stencil: None, @@ -228,20 +211,20 @@ impl Pipeline { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::image vertex buffer"), contents: bytemuck::cast_slice(&QUAD_VERTS), - usage: wgpu::BufferUsage::VERTEX, + usage: wgpu::BufferUsages::VERTEX, }); 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, + usage: wgpu::BufferUsages::INDEX, }); let instances = device.create_buffer(&wgpu::BufferDescriptor { 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, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -423,16 +406,14 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::image render pass"), - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, + color_attachments: &[wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, - ], + }], depth_stencil_attachment: None, }); diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 660ebe44..c1347e55 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -4,6 +4,8 @@ mod allocation; mod allocator; mod layer; +use std::num::NonZeroU32; + pub use allocation::Allocation; pub use entry::Entry; pub use layer::Layer; @@ -24,7 +26,7 @@ impl Atlas { let extent = wgpu::Extent3d { width: SIZE, height: SIZE, - depth: 1, + depth_or_array_layers: 1, }; let texture = device.create_texture(&wgpu::TextureDescriptor { @@ -34,9 +36,9 @@ impl Atlas { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Bgra8UnormSrgb, - usage: wgpu::TextureUsage::COPY_DST - | wgpu::TextureUsage::COPY_SRC - | wgpu::TextureUsage::SAMPLED, + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { @@ -105,7 +107,7 @@ impl Atlas { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::image staging buffer"), contents: &padded_data, - usage: wgpu::BufferUsage::COPY_SRC, + usage: wgpu::BufferUsages::COPY_SRC, }); match &entry { @@ -294,19 +296,19 @@ impl Atlas { let extent = wgpu::Extent3d { width, height, - depth: 1, + depth_or_array_layers: 1, }; encoder.copy_buffer_to_texture( - wgpu::BufferCopyView { + wgpu::ImageCopyBuffer { buffer, - layout: wgpu::TextureDataLayout { + layout: wgpu::ImageDataLayout { offset: offset as u64, - bytes_per_row: 4 * image_width + padding, - rows_per_image: image_height, + bytes_per_row: NonZeroU32::new(4 * image_width + padding), + rows_per_image: NonZeroU32::new(image_height), }, }, - wgpu::TextureCopyView { + wgpu::ImageCopyTexture { texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d { @@ -314,6 +316,7 @@ impl Atlas { y, z: layer as u32, }, + aspect: wgpu::TextureAspect::default(), }, extent, ); @@ -334,15 +337,15 @@ impl Atlas { size: wgpu::Extent3d { width: SIZE, height: SIZE, - depth: self.layers.len() as u32, + depth_or_array_layers: self.layers.len() as u32, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Bgra8UnormSrgb, - usage: wgpu::TextureUsage::COPY_DST - | wgpu::TextureUsage::COPY_SRC - | wgpu::TextureUsage::SAMPLED, + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, }); let amount_to_copy = self.layers.len() - amount; @@ -355,7 +358,7 @@ impl Atlas { } encoder.copy_texture_to_texture( - wgpu::TextureCopyView { + wgpu::ImageCopyTexture { texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d { @@ -363,8 +366,9 @@ impl Atlas { y: 0, z: i as u32, }, + aspect: wgpu::TextureAspect::default(), }, - wgpu::TextureCopyView { + wgpu::ImageCopyTexture { texture: &new_texture, mip_level: 0, origin: wgpu::Origin3d { @@ -372,11 +376,12 @@ impl Atlas { y: 0, z: i as u32, }, + aspect: wgpu::TextureAspect::default(), }, wgpu::Extent3d { width: SIZE, height: SIZE, - depth: 1, + depth_or_array_layers: 1, }, ); } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index ab0f67d0..cd511a45 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,7 +1,8 @@ -use crate::image::atlas::{self, Atlas}; use iced_native::svg; use std::collections::{HashMap, HashSet}; +use crate::image::atlas::{self, Atlas}; + pub enum Svg { Loaded(usvg::Tree), NotFound, @@ -74,8 +75,8 @@ impl Cache { let id = handle.id(); let (width, height) = ( - (scale * width).round() as u32, - (scale * height).round() as u32, + (scale * width).ceil() as u32, + (scale * height).ceil() as u32, ); // TODO: Optimize! @@ -111,29 +112,17 @@ impl Cache { let width = img.width(); let height = img.height(); - let mut rgba = img.take().into_iter(); - - // TODO: Perform conversion in the GPU - let bgra: Vec<u8> = std::iter::from_fn(move || { - use std::iter::once; - - let r = rgba.next()?; - let g = rgba.next()?; - let b = rgba.next()?; - let a = rgba.next()?; - - Some(once(b).chain(once(g)).chain(once(r)).chain(once(a))) - }) - .flatten() - .collect(); + let mut rgba = img.take(); + rgba.chunks_exact_mut(4).for_each(|rgba| rgba.swap(0, 2)); let allocation = texture_atlas.upload( width, height, - bytemuck::cast_slice(bgra.as_slice()), + bytemuck::cast_slice(rgba.as_slice()), device, encoder, )?; + log::debug!("allocating {} {}x{}", id, width, height); let _ = self.svg_hits.insert(id); let _ = self.rasterized_hits.insert((id, width, height)); diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index e0a6e043..148d0f6a 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -23,12 +23,12 @@ impl Pipeline { label: Some("iced_wgpu::quad uniforms layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::VERTEX, + visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: wgpu::BufferSize::new( - mem::size_of::<Uniforms>() as u64, + mem::size_of::<Uniforms>() as wgpu::BufferAddress, ), }, count: None, @@ -37,8 +37,8 @@ impl Pipeline { 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, + size: mem::size_of::<Uniforms>() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -47,11 +47,7 @@ impl Pipeline { layout: &constant_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &constants_buffer, - offset: 0, - size: None, - }, + resource: constants_buffer.as_entire_binding(), }], }); @@ -62,91 +58,68 @@ impl Pipeline { bind_group_layouts: &[&constant_layout], }); - let vs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/quad.vert.spv" - )); - - let fs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/quad.frag.spv" - )); + let shader = + device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu::quad::shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("shader/quad.wgsl"), + )), + }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("iced_wgpu::quad pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", + module: &shader, + entry_point: "vs_main", buffers: &[ wgpu::VertexBufferLayout { array_stride: mem::size_of::<Vertex>() as u64, - step_mode: wgpu::InputStepMode::Vertex, + step_mode: wgpu::VertexStepMode::Vertex, attributes: &[wgpu::VertexAttribute { shader_location: 0, - format: wgpu::VertexFormat::Float2, + format: wgpu::VertexFormat::Float32x2, offset: 0, }], }, wgpu::VertexBufferLayout { array_stride: mem::size_of::<layer::Quad>() as u64, - step_mode: wgpu::InputStepMode::Instance, - attributes: &[ - wgpu::VertexAttribute { - shader_location: 1, - format: wgpu::VertexFormat::Float2, - offset: 0, - }, - wgpu::VertexAttribute { - shader_location: 2, - format: wgpu::VertexFormat::Float2, - offset: 4 * 2, - }, - wgpu::VertexAttribute { - shader_location: 3, - format: wgpu::VertexFormat::Float4, - offset: 4 * (2 + 2), - }, - wgpu::VertexAttribute { - shader_location: 4, - format: wgpu::VertexFormat::Float4, - offset: 4 * (2 + 2 + 4), - }, - wgpu::VertexAttribute { - shader_location: 5, - format: wgpu::VertexFormat::Float, - offset: 4 * (2 + 2 + 4 + 4), - }, - wgpu::VertexAttribute { - shader_location: 6, - format: wgpu::VertexFormat::Float, - offset: 4 * (2 + 2 + 4 + 4 + 1), - }, - ], + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + 1 => Float32x2, + 2 => Float32x2, + 3 => Float32x4, + 4 => Float32x4, + 5 => Float32, + 6 => Float32, + ), }, ], }, fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", + module: &shader, + entry_point: "fs_main", targets: &[wgpu::ColorTargetState { format, - color_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - write_mask: wgpu::ColorWrite::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, }], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Cw, - cull_mode: wgpu::CullMode::None, ..Default::default() }, depth_stencil: None, @@ -161,20 +134,20 @@ impl Pipeline { device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("iced_wgpu::quad vertex buffer"), contents: bytemuck::cast_slice(&QUAD_VERTS), - usage: wgpu::BufferUsage::VERTEX, + usage: wgpu::BufferUsages::VERTEX, }); 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, + usage: wgpu::BufferUsages::INDEX, }); let instances = device.create_buffer(&wgpu::BufferDescriptor { 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, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -237,16 +210,14 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::quad render pass"), - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, + color_attachments: &[wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, - ], + }], depth_stencil_attachment: None, }); @@ -309,6 +280,9 @@ const MAX_INSTANCES: usize = 100_000; struct Uniforms { transform: [f32; 16], scale: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4<f32> which aligns to 16, so align to that + _padding: [f32; 3], } impl Uniforms { @@ -316,6 +290,7 @@ impl Uniforms { Self { transform: *transformation.as_ref(), scale, + _padding: [0.0; 3], } } } @@ -325,6 +300,7 @@ impl Default for Uniforms { Self { transform: *Transformation::identity().as_ref(), scale: 1.0, + _padding: [0.0; 3], } } } diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 26763e22..23b55904 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -6,16 +6,14 @@ pub use crate::Antialiasing; /// [`Backend`]: crate::Backend #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Settings { - /// The output format of the [`Backend`]. - /// - /// [`Backend`]: crate::Backend - pub format: wgpu::TextureFormat, - /// The present mode of the [`Backend`]. /// /// [`Backend`]: crate::Backend pub present_mode: wgpu::PresentMode, + /// The internal graphics backend to use. + pub internal_backend: wgpu::Backends, + /// The bytes of the font that will be used by default. /// /// If `None` is provided, a default system font will be chosen. @@ -26,18 +24,66 @@ pub struct Settings { /// By default, it will be set to 20. pub default_text_size: u16, + /// If enabled, spread text workload in multiple threads when multiple cores + /// are available. + /// + /// By default, it is disabled. + pub text_multithreading: bool, + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. pub antialiasing: Option<Antialiasing>, } +impl Settings { + /// Creates new [`Settings`] using environment configuration. + /// + /// Specifically: + /// + /// - The `internal_backend` can be configured using the `WGPU_BACKEND` + /// environment variable. If the variable is not set, the primary backend + /// will be used. The following values are allowed: + /// - `vulkan` + /// - `metal` + /// - `dx12` + /// - `dx11` + /// - `gl` + /// - `webgpu` + /// - `primary` + pub fn from_env() -> Self { + Settings { + internal_backend: backend_from_env() + .unwrap_or(wgpu::Backends::all()), + ..Self::default() + } + } +} + impl Default for Settings { fn default() -> Settings { Settings { - format: wgpu::TextureFormat::Bgra8UnormSrgb, present_mode: wgpu::PresentMode::Mailbox, + internal_backend: wgpu::Backends::all(), default_font: None, default_text_size: 20, + text_multithreading: false, antialiasing: None, } } } + +fn backend_from_env() -> Option<wgpu::Backends> { + std::env::var("WGPU_BACKEND").ok().map(|backend| { + match backend.to_lowercase().as_str() { + "vulkan" => wgpu::Backends::VULKAN, + "metal" => wgpu::Backends::METAL, + "dx12" => wgpu::Backends::DX12, + "dx11" => wgpu::Backends::DX11, + "gl" => wgpu::Backends::GL, + "webgpu" => wgpu::Backends::BROWSER_WEBGPU, + "primary" => wgpu::Backends::PRIMARY, + other => panic!("Unknown backend: {}", other), + } + }) +} diff --git a/wgpu/src/shader/blit.frag b/wgpu/src/shader/blit.frag deleted file mode 100644 index dfed960f..00000000 --- a/wgpu/src/shader/blit.frag +++ /dev/null @@ -1,12 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 v_Uv; - -layout(set = 0, binding = 0) uniform sampler u_Sampler; -layout(set = 1, binding = 0) uniform texture2D u_Texture; - -layout(location = 0) out vec4 o_Color; - -void main() { - o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv); -} diff --git a/wgpu/src/shader/blit.frag.spv b/wgpu/src/shader/blit.frag.spv Binary files differdeleted file mode 100644 index 2c5638b5..00000000 --- a/wgpu/src/shader/blit.frag.spv +++ /dev/null diff --git a/wgpu/src/shader/blit.vert b/wgpu/src/shader/blit.vert deleted file mode 100644 index 899cd39d..00000000 --- a/wgpu/src/shader/blit.vert +++ /dev/null @@ -1,26 +0,0 @@ -#version 450 - -layout(location = 0) out vec2 o_Uv; - -const vec2 positions[6] = vec2[6]( - vec2(-1.0, 1.0), - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(1.0, 1.0), - vec2(1.0, -1.0) -); - -const vec2 uvs[6] = vec2[6]( - vec2(0.0, 0.0), - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(1.0, 0.0), - vec2(1.0, 1.0) -); - -void main() { - o_Uv = uvs[gl_VertexIndex]; - gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); -} diff --git a/wgpu/src/shader/blit.vert.spv b/wgpu/src/shader/blit.vert.spv Binary files differdeleted file mode 100644 index e0b436ce..00000000 --- a/wgpu/src/shader/blit.vert.spv +++ /dev/null diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl new file mode 100644 index 00000000..694f192e --- /dev/null +++ b/wgpu/src/shader/blit.wgsl @@ -0,0 +1,43 @@ +var positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>( + vec2<f32>(-1.0, 1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>(1.0, -1.0), + vec2<f32>(-1.0, 1.0), + vec2<f32>(1.0, 1.0), + vec2<f32>(1.0, -1.0) +); + +var uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>( + vec2<f32>(0.0, 0.0), + vec2<f32>(0.0, 1.0), + vec2<f32>(1.0, 1.0), + vec2<f32>(0.0, 0.0), + vec2<f32>(1.0, 0.0), + vec2<f32>(1.0, 1.0) +); + +[[group(0), binding(0)]] var u_sampler: sampler; +[[group(1), binding(0)]] var u_texture: texture_2d<f32>; + +struct VertexInput { + [[builtin(vertex_index)]] vertex_index: u32; +}; + +struct VertexOutput { + [[builtin(position)]] position: vec4<f32>; + [[location(0)]] uv: vec2<f32>; +}; + +[[stage(vertex)]] +fn vs_main(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.uv = uvs[input.vertex_index]; + out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0); + + return out; +} + +[[stage(fragment)]] +fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { + return textureSample(u_texture, u_sampler, input.uv); +} diff --git a/wgpu/src/shader/image.frag b/wgpu/src/shader/image.frag deleted file mode 100644 index 2809e9e6..00000000 --- a/wgpu/src/shader/image.frag +++ /dev/null @@ -1,12 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 v_Uv; - -layout(set = 0, binding = 1) uniform sampler u_Sampler; -layout(set = 1, binding = 0) uniform texture2DArray u_Texture; - -layout(location = 0) out vec4 o_Color; - -void main() { - o_Color = texture(sampler2DArray(u_Texture, u_Sampler), v_Uv); -} diff --git a/wgpu/src/shader/image.frag.spv b/wgpu/src/shader/image.frag.spv Binary files differdeleted file mode 100644 index 65b08aa3..00000000 --- a/wgpu/src/shader/image.frag.spv +++ /dev/null diff --git a/wgpu/src/shader/image.vert b/wgpu/src/shader/image.vert deleted file mode 100644 index dab53cfe..00000000 --- a/wgpu/src/shader/image.vert +++ /dev/null @@ -1,27 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 v_Pos; -layout(location = 1) in vec2 i_Pos; -layout(location = 2) in vec2 i_Scale; -layout(location = 3) in vec2 i_Atlas_Pos; -layout(location = 4) in vec2 i_Atlas_Scale; -layout(location = 5) in uint i_Layer; - -layout (set = 0, binding = 0) uniform Globals { - mat4 u_Transform; -}; - -layout(location = 0) out vec3 o_Uv; - -void main() { - o_Uv = vec3(v_Pos * i_Atlas_Scale + i_Atlas_Pos, i_Layer); - - mat4 i_Transform = mat4( - vec4(i_Scale.x, 0.0, 0.0, 0.0), - vec4(0.0, i_Scale.y, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(i_Pos, 0.0, 1.0) - ); - - gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0); -} diff --git a/wgpu/src/shader/image.vert.spv b/wgpu/src/shader/image.vert.spv Binary files differdeleted file mode 100644 index 21f5db2d..00000000 --- a/wgpu/src/shader/image.vert.spv +++ /dev/null diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl new file mode 100644 index 00000000..a63ee8f6 --- /dev/null +++ b/wgpu/src/shader/image.wgsl @@ -0,0 +1,47 @@ +[[block]] +struct Globals { + transform: mat4x4<f32>; +}; + +[[group(0), binding(0)]] var<uniform> globals: Globals; +[[group(0), binding(1)]] var u_sampler: sampler; +[[group(1), binding(0)]] var u_texture: texture_2d_array<f32>; + +struct VertexInput { + [[location(0)]] v_pos: vec2<f32>; + [[location(1)]] pos: vec2<f32>; + [[location(2)]] scale: vec2<f32>; + [[location(3)]] atlas_pos: vec2<f32>; + [[location(4)]] atlas_scale: vec2<f32>; + [[location(5)]] layer: i32; +}; + +struct VertexOutput { + [[builtin(position)]] position: vec4<f32>; + [[location(0)]] uv: vec2<f32>; + [[location(1)]] layer: f32; // this should be an i32, but naga currently reads that as requiring interpolation. +}; + +[[stage(vertex)]] +fn vs_main(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + + out.uv = vec2<f32>(input.v_pos * input.atlas_scale + input.atlas_pos); + out.layer = f32(input.layer); + + var transform: mat4x4<f32> = mat4x4<f32>( + vec4<f32>(input.scale.x, 0.0, 0.0, 0.0), + vec4<f32>(0.0, input.scale.y, 0.0, 0.0), + vec4<f32>(0.0, 0.0, 1.0, 0.0), + vec4<f32>(input.pos, 0.0, 1.0) + ); + + out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); + + return out; +} + +[[stage(fragment)]] +fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); +} diff --git a/wgpu/src/shader/quad.frag b/wgpu/src/shader/quad.frag deleted file mode 100644 index ad1af1ad..00000000 --- a/wgpu/src/shader/quad.frag +++ /dev/null @@ -1,66 +0,0 @@ -#version 450 - -layout(location = 0) in vec4 v_Color; -layout(location = 1) in vec4 v_BorderColor; -layout(location = 2) in vec2 v_Pos; -layout(location = 3) in vec2 v_Scale; -layout(location = 4) in float v_BorderRadius; -layout(location = 5) in float v_BorderWidth; - -layout(location = 0) out vec4 o_Color; - -float 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), - max(max(top_left_distance.y, bottom_right_distance.y), 0) - ); - - return sqrt(distance.x * distance.x + distance.y * distance.y); -} - -void main() { - vec4 mixed_color; - - // TODO: Remove branching (?) - if(v_BorderWidth > 0) { - float internal_border = max(v_BorderRadius - v_BorderWidth, 0); - - float internal_distance = distance( - gl_FragCoord.xy, - v_Pos + vec2(v_BorderWidth), - v_Scale - vec2(v_BorderWidth * 2.0), - internal_border - ); - - float border_mix = smoothstep( - max(internal_border - 0.5, 0.0), - internal_border + 0.5, - internal_distance - ); - - mixed_color = mix(v_Color, v_BorderColor, border_mix); - } else { - mixed_color = v_Color; - } - - float d = distance( - gl_FragCoord.xy, - v_Pos, - v_Scale, - v_BorderRadius - ); - - float radius_alpha = - 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0), v_BorderRadius + 0.5, d); - - o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); -} diff --git a/wgpu/src/shader/quad.frag.spv b/wgpu/src/shader/quad.frag.spv Binary files differdeleted file mode 100644 index 519f5f01..00000000 --- a/wgpu/src/shader/quad.frag.spv +++ /dev/null diff --git a/wgpu/src/shader/quad.vert b/wgpu/src/shader/quad.vert deleted file mode 100644 index 09a278b1..00000000 --- a/wgpu/src/shader/quad.vert +++ /dev/null @@ -1,47 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 v_Pos; -layout(location = 1) in vec2 i_Pos; -layout(location = 2) in vec2 i_Scale; -layout(location = 3) in vec4 i_Color; -layout(location = 4) in vec4 i_BorderColor; -layout(location = 5) in float i_BorderRadius; -layout(location = 6) in float i_BorderWidth; - -layout (set = 0, binding = 0) uniform Globals { - mat4 u_Transform; - float u_Scale; -}; - -layout(location = 0) out vec4 o_Color; -layout(location = 1) out vec4 o_BorderColor; -layout(location = 2) out vec2 o_Pos; -layout(location = 3) out vec2 o_Scale; -layout(location = 4) out float o_BorderRadius; -layout(location = 5) out float o_BorderWidth; - -void main() { - vec2 p_Pos = i_Pos * u_Scale; - 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) - ); - - o_Color = i_Color; - o_BorderColor = i_BorderColor; - o_Pos = p_Pos; - o_Scale = p_Scale; - o_BorderRadius = i_BorderRadius * u_Scale; - o_BorderWidth = i_BorderWidth * u_Scale; - - gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0); -} diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv Binary files differdeleted file mode 100644 index fa71ba1e..00000000 --- a/wgpu/src/shader/quad.vert.spv +++ /dev/null diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl new file mode 100644 index 00000000..80d733ab --- /dev/null +++ b/wgpu/src/shader/quad.wgsl @@ -0,0 +1,122 @@ +[[block]] +struct Globals { + transform: mat4x4<f32>; + scale: f32; +}; + +[[group(0), binding(0)]] var<uniform> globals: Globals; + +struct VertexInput { + [[location(0)]] v_pos: vec2<f32>; + [[location(1)]] pos: vec2<f32>; + [[location(2)]] scale: vec2<f32>; + [[location(3)]] color: vec4<f32>; + [[location(4)]] border_color: vec4<f32>; + [[location(5)]] border_radius: f32; + [[location(6)]] border_width: f32; +}; + +struct VertexOutput { + [[builtin(position)]] position: vec4<f32>; + [[location(0)]] color: vec4<f32>; + [[location(1)]] border_color: vec4<f32>; + [[location(2)]] pos: vec2<f32>; + [[location(3)]] scale: vec2<f32>; + [[location(4)]] border_radius: f32; + [[location(5)]] border_width: f32; +}; + +[[stage(vertex)]] +fn vs_main(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + + var pos: vec2<f32> = input.pos * globals.scale; + var scale: vec2<f32> = input.scale * globals.scale; + + var border_radius: f32 = min( + input.border_radius, + min(input.scale.x, input.scale.y) / 2.0 + ); + + var transform: mat4x4<f32> = mat4x4<f32>( + vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0), + vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0), + vec4<f32>(0.0, 0.0, 1.0, 0.0), + vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) + ); + + out.color = input.color; + out.border_color = input.border_color; + out.pos = pos; + out.scale = scale; + out.border_radius = border_radius * globals.scale; + out.border_width = input.border_width * globals.scale; + out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0); + + return out; +} + +fn distance_alg( + frag_coord: vec2<f32>, + position: vec2<f32>, + size: vec2<f32>, + radius: f32 +) -> f32 { + var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0; + var top_left: vec2<f32> = position + vec2<f32>(radius, radius); + var bottom_right: vec2<f32> = top_left + inner_size; + + var top_left_distance: vec2<f32> = top_left - frag_coord; + var bottom_right_distance: vec2<f32> = frag_coord - bottom_right; + + var dist: vec2<f32> = vec2<f32>( + 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(dist.x * dist.x + dist.y * dist.y); +} + + +[[stage(fragment)]] +fn fs_main( + input: VertexOutput +) -> [[location(0)]] vec4<f32> { + var mixed_color: vec4<f32> = input.color; + + if (input.border_width > 0.0) { + var internal_border: f32 = max( + input.border_radius - input.border_width, + 0.0 + ); + + var internal_distance: f32 = distance_alg( + vec2<f32>(input.position.x, input.position.y), + input.pos + vec2<f32>(input.border_width, input.border_width), + input.scale - vec2<f32>(input.border_width * 2.0, input.border_width * 2.0), + internal_border + ); + + var border_mix: f32 = smoothStep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(input.color, input.border_color, vec4<f32>(border_mix, border_mix, border_mix, border_mix)); + } + + var dist: f32 = distance_alg( + vec2<f32>(input.position.x, input.position.y), + input.pos, + input.scale, + input.border_radius + ); + + var radius_alpha: f32 = 1.0 - smoothStep( + max(input.border_radius - 0.5, 0.0), + input.border_radius + 0.5, + dist); + + return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); +} diff --git a/wgpu/src/shader/triangle.frag b/wgpu/src/shader/triangle.frag deleted file mode 100644 index e39c45e7..00000000 --- a/wgpu/src/shader/triangle.frag +++ /dev/null @@ -1,8 +0,0 @@ -#version 450 - -layout(location = 0) in vec4 i_Color; -layout(location = 0) out vec4 o_Color; - -void main() { - o_Color = i_Color; -} diff --git a/wgpu/src/shader/triangle.frag.spv b/wgpu/src/shader/triangle.frag.spv Binary files differdeleted file mode 100644 index 11201872..00000000 --- a/wgpu/src/shader/triangle.frag.spv +++ /dev/null diff --git a/wgpu/src/shader/triangle.vert b/wgpu/src/shader/triangle.vert deleted file mode 100644 index 1f2c009b..00000000 --- a/wgpu/src/shader/triangle.vert +++ /dev/null @@ -1,15 +0,0 @@ -#version 450 - -layout(location = 0) in vec2 i_Position; -layout(location = 1) in vec4 i_Color; - -layout(location = 0) out vec4 o_Color; - -layout (set = 0, binding = 0) uniform Globals { - mat4 u_Transform; -}; - -void main() { - gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); - o_Color = i_Color; -} diff --git a/wgpu/src/shader/triangle.vert.spv b/wgpu/src/shader/triangle.vert.spv Binary files differdeleted file mode 100644 index 871f4f55..00000000 --- a/wgpu/src/shader/triangle.vert.spv +++ /dev/null diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl new file mode 100644 index 00000000..96eaabcc --- /dev/null +++ b/wgpu/src/shader/triangle.wgsl @@ -0,0 +1,31 @@ +[[block]] +struct Globals { + transform: mat4x4<f32>; +}; + +[[group(0), binding(0)]] var<uniform> globals: Globals; + +struct VertexInput { + [[location(0)]] position: vec2<f32>; + [[location(1)]] color: vec4<f32>; +}; + +struct VertexOutput { + [[builtin(position)]] position: vec4<f32>; + [[location(0)]] color: vec4<f32>; +}; + +[[stage(vertex)]] +fn vs_main(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + + out.color = input.color; + out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0); + + return out; +} + +[[stage(fragment)]] +fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { + return input.color; +} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 4d92d9e9..ee49ee4b 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,8 +1,12 @@ use crate::Transformation; + use iced_graphics::font; + use std::{cell::RefCell, collections::HashMap}; use wgpu_glyph::ab_glyph; +pub use iced_native::text::Hit; + #[derive(Debug)] pub struct Pipeline { draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>, @@ -15,6 +19,7 @@ impl Pipeline { device: &wgpu::Device, format: wgpu::TextureFormat, default_font: Option<&[u8]>, + multithreading: bool, ) -> Self { let default_font = default_font.map(|slice| slice.to_vec()); @@ -46,7 +51,7 @@ impl Pipeline { 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 + .draw_cache_multithread(multithreading) .build(device, format); let measure_brush = @@ -116,6 +121,94 @@ impl Pipeline { } } + pub fn hit_test( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, + nearest_only: bool, + ) -> Hit { + use wgpu_glyph::GlyphCruncher; + + let wgpu_glyph::FontId(font_id) = self.find_font(font); + + let section = wgpu_glyph::Section { + bounds: (bounds.width, bounds.height), + text: vec![wgpu_glyph::Text { + text: content, + scale: size.into(), + font_id: wgpu_glyph::FontId(font_id), + extra: wgpu_glyph::Extra::default(), + }], + ..Default::default() + }; + + let mut mb = self.measure_brush.borrow_mut(); + + // The underlying type is FontArc, so clones are cheap. + use wgpu_glyph::ab_glyph::{Font, ScaleFont}; + let font = mb.fonts()[font_id].clone().into_scaled(size); + + // Implements an iterator over the glyph bounding boxes. + let bounds = mb.glyphs(section).map( + |wgpu_glyph::SectionGlyph { + byte_index, glyph, .. + }| { + ( + *byte_index, + iced_native::Rectangle::new( + iced_native::Point::new( + glyph.position.x - font.h_side_bearing(glyph.id), + glyph.position.y - font.ascent(), + ), + iced_native::Size::new( + font.h_advance(glyph.id), + font.ascent() - font.descent(), + ), + ), + ) + }, + ); + + // Implements computation of the character index based on the byte index + // within the input string. + let char_index = |byte_index| { + let mut b_count = 0; + for (i, utf8_len) in + content.chars().map(|c| c.len_utf8()).enumerate() + { + if byte_index < (b_count + utf8_len) { + return i; + } + b_count += utf8_len; + } + return byte_index; + }; + + if !nearest_only { + for (idx, bounds) in bounds.clone() { + if bounds.contains(point) { + return Hit::CharOffset(char_index(idx)); + } + } + } + + let (idx, nearest) = bounds.fold( + (0usize, iced_native::Point::ORIGIN), + |acc: (usize, iced_native::Point), (idx, bounds)| { + if bounds.center().distance(point) < acc.1.distance(point) { + (idx, bounds.center()) + } else { + acc + } + }, + ); + + Hit::NearestCharOffset(char_index(idx), (point - nearest).into()) + } + 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. diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 2f255940..2aaebe58 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -29,7 +29,7 @@ struct Buffer<T> { label: &'static str, raw: wgpu::Buffer, size: usize, - usage: wgpu::BufferUsage, + usage: wgpu::BufferUsages, _type: std::marker::PhantomData<T>, } @@ -38,7 +38,7 @@ impl<T> Buffer<T> { label: &'static str, device: &wgpu::Device, size: usize, - usage: wgpu::BufferUsage, + usage: wgpu::BufferUsages, ) -> Self { let raw = device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), @@ -85,7 +85,7 @@ impl Pipeline { label: Some("iced_wgpu::triangle uniforms layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::VERTEX, + visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, @@ -101,7 +101,7 @@ impl Pipeline { "iced_wgpu::triangle uniforms buffer", device, UNIFORM_BUFFER_SIZE, - wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, ); let constant_bind_group = @@ -110,13 +110,17 @@ impl Pipeline { layout: &constants_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &constants_buffer.raw, - offset: 0, - size: wgpu::BufferSize::new( - std::mem::size_of::<Uniforms>() as u64, - ), - }, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &constants_buffer.raw, + offset: 0, + size: wgpu::BufferSize::new(std::mem::size_of::< + Uniforms, + >( + ) + as u64), + }, + ), }], }); @@ -127,62 +131,55 @@ impl Pipeline { bind_group_layouts: &[&constants_layout], }); - let vs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/triangle.vert.spv" - )); - - let fs_module = device.create_shader_module(&wgpu::include_spirv!( - "shader/triangle.frag.spv" - )); + let shader = + device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu::triangle::shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("shader/triangle.wgsl"), + )), + }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("iced_wgpu::triangle pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", + module: &shader, + entry_point: "vs_main", buffers: &[wgpu::VertexBufferLayout { array_stride: mem::size_of::<Vertex2D>() as u64, - step_mode: wgpu::InputStepMode::Vertex, - attributes: &[ + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( // Position - wgpu::VertexAttribute { - shader_location: 0, - format: wgpu::VertexFormat::Float2, - offset: 0, - }, + 0 => Float32x2, // Color - wgpu::VertexAttribute { - shader_location: 1, - format: wgpu::VertexFormat::Float4, - offset: 4 * 2, - }, - ], + 1 => Float32x4, + ), }], }, fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", + module: &shader, + entry_point: "fs_main", targets: &[wgpu::ColorTargetState { format, - color_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - write_mask: wgpu::ColorWrite::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, }], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Cw, - cull_mode: wgpu::CullMode::None, ..Default::default() }, depth_stencil: None, @@ -205,13 +202,13 @@ impl Pipeline { "iced_wgpu::triangle vertex buffer", device, VERTEX_BUFFER_SIZE, - wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, ), index_buffer: Buffer::new( "iced_wgpu::triangle index buffer", device, INDEX_BUFFER_SIZE, - wgpu::BufferUsage::INDEX | wgpu::BufferUsage::COPY_DST, + wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, ), } } @@ -254,15 +251,15 @@ impl Pipeline { layout: &self.constants_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &self.uniforms_buffer.raw, - offset: 0, - size: wgpu::BufferSize::new(std::mem::size_of::< - Uniforms, - >( - ) - as u64), - }, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: &self.uniforms_buffer.raw, + offset: 0, + size: wgpu::BufferSize::new( + std::mem::size_of::<Uniforms>() as u64, + ), + }, + ), }], }); } @@ -363,13 +360,11 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::triangle render pass"), - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment, - resolve_target, - ops: wgpu::Operations { load, store: true }, - }, - ], + color_attachments: &[wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { load, store: true }, + }], depth_stencil_attachment: None, }); diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index d964f815..070d2d93 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -20,9 +20,9 @@ impl Blit { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); @@ -31,7 +31,7 @@ impl Blit { label: Some("iced_wgpu::triangle:msaa uniforms layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::FRAGMENT, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler { comparison: false, filtering: false, @@ -55,7 +55,7 @@ impl Blit { label: Some("iced_wgpu::triangle::msaa texture layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStage::FRAGMENT, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false, @@ -74,45 +74,46 @@ impl Blit { bind_group_layouts: &[&constant_layout, &texture_layout], }); - let vs_module = device.create_shader_module(&wgpu::include_spirv!( - "../shader/blit.vert.spv" - )); - - let fs_module = device.create_shader_module(&wgpu::include_spirv!( - "../shader/blit.frag.spv" - )); + let shader = + device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu::triangle::blit_shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shader/blit.wgsl"), + )), + }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("iced_wgpu::triangle::msaa pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", + module: &shader, + entry_point: "vs_main", buffers: &[], }, fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", + module: &shader, + entry_point: "fs_main", targets: &[wgpu::ColorTargetState { format, - color_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha_blend: wgpu::BlendState { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - write_mask: wgpu::ColorWrite::ALL, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, }], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Cw, - cull_mode: wgpu::CullMode::None, ..Default::default() }, depth_stencil: None, @@ -177,16 +178,14 @@ impl Blit { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::triangle::msaa render pass"), - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, + color_attachments: &[wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, }, - ], + }], depth_stencil_attachment: None, }); @@ -222,7 +221,7 @@ impl Targets { let extent = wgpu::Extent3d { width, height, - depth: 1, + depth_or_array_layers: 1, }; let attachment = device.create_texture(&wgpu::TextureDescriptor { @@ -232,7 +231,7 @@ impl Targets { sample_count, dimension: wgpu::TextureDimension::D2, format, - usage: wgpu::TextureUsage::RENDER_ATTACHMENT, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }); let resolve = device.create_texture(&wgpu::TextureDescriptor { @@ -242,8 +241,8 @@ impl Targets { sample_count: 1, dimension: wgpu::TextureDimension::D2, format, - usage: wgpu::TextureUsage::RENDER_ATTACHMENT - | wgpu::TextureUsage::SAMPLED, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, }); let attachment = diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index 304bb726..a575d036 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -20,6 +20,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod text_input; +pub mod toggler; pub mod tooltip; #[doc(no_inline)] @@ -45,6 +46,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] +pub use toggler::Toggler; +#[doc(no_inline)] pub use tooltip::Tooltip; #[cfg(feature = "canvas")] diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs index 44f9201c..fc36862c 100644 --- a/wgpu/src/widget/pane_grid.rs +++ b/wgpu/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! 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 +//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid use crate::Renderer; pub use iced_graphics::pane_grid::{ diff --git a/wgpu/src/widget/toggler.rs b/wgpu/src/widget/toggler.rs new file mode 100644 index 00000000..dfcf759b --- /dev/null +++ b/wgpu/src/widget/toggler.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using togglers. +use crate::Renderer; + +pub use iced_graphics::toggler::{Style, StyleSheet}; + +/// A toggler that can be toggled +/// +/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. +pub type Toggler<Message> = iced_native::Toggler<Message, Renderer>; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 36d6ab1a..eca54b6f 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -13,6 +13,7 @@ pub struct Compositor { queue: wgpu::Queue, staging_belt: wgpu::util::StagingBelt, local_pool: futures::executor::LocalPool, + format: wgpu::TextureFormat, } impl Compositor { @@ -21,8 +22,15 @@ impl Compositor { /// 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); + pub async fn request<W: HasRawWindowHandle>( + settings: Settings, + compatible_window: Option<&W>, + ) -> Option<Self> { + let instance = wgpu::Instance::new(settings.internal_backend); + + #[allow(unsafe_code)] + let compatible_surface = compatible_window + .map(|window| unsafe { instance.create_surface(window) }); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { @@ -31,10 +39,14 @@ impl Compositor { } else { wgpu::PowerPreference::HighPerformance }, - compatible_surface: None, + compatible_surface: compatible_surface.as_ref(), }) .await?; + let format = compatible_surface + .as_ref() + .and_then(|surface| surface.get_preferred_format(&adapter))?; + let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { @@ -62,12 +74,13 @@ impl Compositor { queue, staging_belt, local_pool, + format, }) } /// Creates a new rendering [`Backend`] for this [`Compositor`]. pub fn create_backend(&self) -> Backend { - Backend::new(&self.device, self.settings) + Backend::new(&self.device, self.settings, self.format) } } @@ -75,11 +88,16 @@ 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)?; + fn new<W: HasRawWindowHandle>( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Renderer), Error> { + let compositor = futures::executor::block_on(Self::request( + settings, + compatible_window, + )) + .ok_or(Error::AdapterNotFound)?; let backend = compositor.create_backend(); @@ -96,85 +114,110 @@ impl iced_graphics::window::Compositor for Compositor { } } - fn create_swap_chain( + fn configure_surface( &mut self, - surface: &Self::Surface, + surface: &mut Self::Surface, width: u32, height: u32, - ) -> Self::SwapChain { - self.device.create_swap_chain( - surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::RENDER_ATTACHMENT, - format: self.settings.format, + ) { + surface.configure( + &self.device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: self.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, + surface: &mut Self::Surface, 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"), + ) -> Result<mouse::Interaction, iced_graphics::window::SurfaceError> { + match surface.get_current_frame() { + Ok(frame) => { + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + let view = &frame + .output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let _ = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some( + "iced_wgpu::window::Compositor render pass", + ), + color_attachments: &[wgpu::RenderPassColorAttachment { + 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, + 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(); + + Ok(mouse_interaction) + } + Err(error) => match error { + wgpu::SurfaceError::Timeout => { + Err(iced_graphics::window::SurfaceError::Timeout) + } + wgpu::SurfaceError::Outdated => { + Err(iced_graphics::window::SurfaceError::Outdated) + } + wgpu::SurfaceError::Lost => { + Err(iced_graphics::window::SurfaceError::Lost) + } + wgpu::SurfaceError::OutOfMemory => { + Err(iced_graphics::window::SurfaceError::OutOfMemory) + } }, - ); - - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::window::Compositor render pass"), - 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/winit/Cargo.toml b/winit/Cargo.toml index ecee0e2e..6eca624d 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_winit" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" description = "A winit runtime for Iced" @@ -14,21 +14,25 @@ categories = ["gui"] debug = ["iced_native/debug"] [dependencies] -winit = "0.24" window_clipboard = "0.2" log = "0.4" thiserror = "1.0" +[dependencies.winit] +version = "0.25" +git = "https://github.com/iced-rs/winit" +rev = "24a75a7bdc10d2dc2f520c36284618e9ae6c2568" + [dependencies.iced_native] -version = "0.3" +version = "0.4" path = "../native" [dependencies.iced_graphics] -version = "0.1" +version = "0.2" path = "../graphics" [dependencies.iced_futures] -version = "0.2" +version = "0.3" path = "../futures" [target.'cfg(target_os = "windows")'.dependencies.winapi] diff --git a/winit/README.md b/winit/README.md index 721baa14..58c782c6 100644 --- a/winit/README.md +++ b/winit/README.md @@ -20,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.2" +iced_winit = "0.3" ``` __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 d7d7660e..b683e592 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -14,6 +14,7 @@ use iced_futures::futures; use iced_futures::futures::channel::mpsc; use iced_graphics::window; use iced_native::program::Program; +use iced_native::Menu; use iced_native::{Cache, UserInterface}; use std::mem::ManuallyDrop; @@ -91,6 +92,20 @@ pub trait Application: Program<Clipboard = Clipboard> { fn scale_factor(&self) -> f64 { 1.0 } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// Returns the current system [`Menu`] of the [`Application`]. + /// + /// By default, it returns an empty [`Menu`]. + fn menu(&self) -> Menu<Self::Message> { + Menu::new() + } } /// Runs an [`Application`] with an executor, compositor, and the provided @@ -111,8 +126,6 @@ where let mut debug = Debug::new(); debug.startup_started(); - let (compositor, renderer) = C::new(compositor_settings)?; - let event_loop = EventLoop::with_user_event(); let mut runtime = { @@ -139,20 +152,25 @@ where &application.title(), application.mode(), event_loop.primary_monitor(), + settings.id, ) + .with_menu(Some(conversion::menu(&application.menu()))) .build(&event_loop) .map_err(Error::WindowCreationFailed)?; + let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::<A, E, C>( application, compositor, renderer, - window, runtime, debug, receiver, + window, + settings.exit_on_close_request, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -164,7 +182,22 @@ where return; } - if let Some(event) = event.to_static() { + let event = match event { + winit::event::Event::WindowEvent { + event: + winit::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + }, + window_id, + } => Some(winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(*new_inner_size), + window_id, + }), + _ => event.to_static(), + }; + + if let Some(event) = event { sender.start_send(event).expect("Send event"); let poll = instance.as_mut().poll(&mut context); @@ -181,10 +214,11 @@ 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>>, + window: winit::window::Window, + exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -193,20 +227,19 @@ async fn run_instance<A, E, C>( use iced_futures::futures::stream::StreamExt; use winit::event; - let surface = compositor.create_surface(&window); + let mut surface = compositor.create_surface(&window); let mut clipboard = Clipboard::connect(&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 physical_size = state.physical_size(); + + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); let mut user_interface = ManuallyDrop::new(build_user_interface( &mut application, @@ -264,6 +297,8 @@ async fn run_instance<A, E, C>( // Update window state.synchronize(&application, &window); + let should_exit = application.should_exit(); + user_interface = ManuallyDrop::new(build_user_interface( &mut application, cache, @@ -271,6 +306,10 @@ async fn run_instance<A, E, C>( state.logical_size(), &mut debug, )); + + if should_exit { + break; + } } debug.draw_started(); @@ -280,15 +319,30 @@ async fn run_instance<A, E, C>( window.request_redraw(); } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use iced_native::event; + events.push(iced_native::Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } event::Event::UserEvent(message) => { messages.push(message); } event::Event::RedrawRequested(_) => { + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + 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(); @@ -303,8 +357,8 @@ async fn run_instance<A, E, C>( .draw(&mut renderer, state.cursor_position()); debug.draw_finished(); - swap_chain = compositor.create_swap_chain( - &surface, + compositor.configure_surface( + &mut surface, physical_size.width, physical_size.height, ); @@ -312,33 +366,61 @@ async fn run_instance<A, E, C>( viewport_version = current_viewport_version; } - let new_mouse_interaction = compositor.draw( + match compositor.draw( &mut renderer, - &mut swap_chain, + &mut surface, state.viewport(), state.background_color(), &primitive, &debug.overlay(), - ); - - debug.render_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; + ) { + Ok(new_mouse_interaction) => { + debug.render_finished(); + + if new_mouse_interaction != mouse_interaction { + 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. + } + Err(error) => match error { + // This is an unrecoverable error. + window::SurfaceError::OutOfMemory => { + panic!("{}", error); + } + _ => { + debug.render_finished(); + + // Try rendering again next frame. + window.request_redraw(); + } + }, + } + } + event::Event::WindowEvent { + event: event::WindowEvent::MenuEntryActivated(entry_id), + .. + } => { + if let Some(message) = + conversion::menu_message(state.menu(), entry_id) + { + messages.push(message); } - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. } event::Event::WindowEvent { event: window_event, .. } => { - if requests_exit(&window_event, state.modifiers()) { + if requests_exit(&window_event, state.modifiers()) + && exit_on_close_request + { break; } diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index 46297370..f60f09be 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -1,5 +1,5 @@ use crate::conversion; -use crate::{Application, Color, Debug, Mode, Point, Size, Viewport}; +use crate::{Application, Color, Debug, Menu, Mode, Point, Size, Viewport}; use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; @@ -9,6 +9,7 @@ use winit::window::Window; #[derive(Debug, Clone)] pub struct State<A: Application> { title: String, + menu: Menu<A::Message>, mode: Mode, background_color: Color, scale_factor: f64, @@ -23,6 +24,7 @@ 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 menu = application.menu(); let mode = application.mode(); let background_color = application.background_color(); let scale_factor = application.scale_factor(); @@ -38,6 +40,7 @@ impl<A: Application> State<A> { Self { title, + menu, mode, background_color, scale_factor, @@ -50,6 +53,11 @@ impl<A: Application> State<A> { } } + /// Returns the current [`Menu`] of the [`State`]. + pub fn menu(&self) -> &Menu<A::Message> { + &self.menu + } + /// Returns the current background [`Color`] of the [`State`]. pub fn background_color(&self) -> Color { self.background_color @@ -182,6 +190,8 @@ impl<A: Application> State<A> { new_mode, )); + window.set_visible(conversion::visible(new_mode)); + self.mode = new_mode; } @@ -201,5 +211,14 @@ impl<A: Application> State<A> { self.scale_factor = new_scale_factor; } + + // Update menu + let new_menu = application.menu(); + + if self.menu != new_menu { + window.set_menu(Some(conversion::menu(&new_menu))); + + self.menu = new_menu; + } } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0e04b35d..8e2dd02d 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -3,10 +3,11 @@ //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native use crate::keyboard; +use crate::menu::{self, Menu}; use crate::mouse; use crate::touch; use crate::window; -use crate::{Event, Mode, Point}; +use crate::{Event, Mode, Point, Position}; /// Converts a winit window event into an iced event. pub fn window_event( @@ -33,6 +34,9 @@ pub fn window_event( height: logical_size.height, })) } + WindowEvent::CloseRequested => { + Some(Event::Window(window::Event::CloseRequested)) + } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::<f64>(scale_factor); @@ -126,10 +130,59 @@ pub fn window_event( WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(*touch, scale_factor))) } + WindowEvent::Moved(position) => { + let winit::dpi::LogicalPosition { x, y } = + position.to_logical(scale_factor); + + Some(Event::Window(window::Event::Moved { x, y })) + } _ => None, } } +/// Converts a [`Position`] to a [`winit`] logical position for a given monitor. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +pub fn position( + monitor: Option<&winit::monitor::MonitorHandle>, + (width, height): (u32, u32), + position: Position, +) -> Option<winit::dpi::Position> { + match position { + Position::Default => None, + Position::Specific(x, y) => { + Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { + x: f64::from(x), + y: f64::from(y), + })) + } + Position::Centered => { + if let Some(monitor) = monitor { + let start = monitor.position(); + + let resolution: winit::dpi::LogicalSize<f64> = + monitor.size().to_logical(monitor.scale_factor()); + + let centered: winit::dpi::PhysicalPosition<i32> = + winit::dpi::LogicalPosition { + x: (resolution.width - f64::from(width)) / 2.0, + y: (resolution.height - f64::from(height)) / 2.0, + } + .to_physical(monitor.scale_factor()); + + Some(winit::dpi::Position::Physical( + winit::dpi::PhysicalPosition { + x: start.x + centered.x, + y: start.y + centered.y, + }, + )) + } else { + None + } + } + } +} + /// Converts a [`Mode`] to a [`winit`] fullscreen mode. /// /// [`winit`]: https://github.com/rust-windowing/winit @@ -138,13 +191,125 @@ pub fn fullscreen( mode: Mode, ) -> Option<winit::window::Fullscreen> { match mode { - Mode::Windowed => None, + Mode::Windowed | Mode::Hidden => None, Mode::Fullscreen => { Some(winit::window::Fullscreen::Borderless(monitor)) } } } +/// Converts a [`Mode`] to a visibility flag. +pub fn visible(mode: Mode) -> bool { + match mode { + Mode::Windowed | Mode::Fullscreen => true, + Mode::Hidden => false, + } +} + +/// Converts a `Hotkey` from [`iced_native`] to a [`winit`] Hotkey. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +fn hotkey(hotkey: keyboard::Hotkey) -> winit::window::Hotkey { + use winit::event::ModifiersState; + + let mut modifiers = ModifiersState::empty(); + modifiers.set(ModifiersState::CTRL, hotkey.modifiers.control()); + modifiers.set(ModifiersState::SHIFT, hotkey.modifiers.shift()); + modifiers.set(ModifiersState::ALT, hotkey.modifiers.alt()); + modifiers.set(ModifiersState::LOGO, hotkey.modifiers.logo()); + + winit::window::Hotkey::new(modifiers, to_virtual_keycode(hotkey.key)) +} + +/// Converts a `Menu` from [`iced_native`] to a [`winit`] menu. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +pub fn menu<Message>(menu: &Menu<Message>) -> winit::window::Menu { + fn menu_i<Message>( + converted: &mut winit::window::Menu, + starting_id: u16, + menu: &Menu<Message>, + ) -> u16 { + let mut id = starting_id; + + for item in menu.iter() { + match item { + menu::Entry::Item { title, hotkey, .. } => { + converted.add_item(id, title, hotkey.map(self::hotkey)); + + id += 1; + } + menu::Entry::Dropdown { title, submenu } => { + let mut converted_submenu = winit::window::Menu::new(); + let n_children = + menu_i(&mut converted_submenu, id, submenu); + + converted.add_dropdown(title, converted_submenu); + + id += n_children; + } + menu::Entry::Separator => { + converted.add_separator(); + } + } + } + + id - starting_id + } + + let mut converted = winit::window::Menu::default(); + let _ = menu_i(&mut converted, 0, menu); + + converted +} + +/// Given a [`Menu`] and an identifier of a [`menu::Entry`], it returns the +/// `Message` that should be produced when that entry is activated. +pub fn menu_message<Message>(menu: &Menu<Message>, id: u16) -> Option<Message> +where + Message: Clone, +{ + fn find_message<Message>( + target: u16, + starting_id: u16, + menu: &Menu<Message>, + ) -> Result<Message, u16> + where + Message: Clone, + { + let mut id = starting_id; + + for entry in menu.iter() { + match entry { + menu::Entry::Item { on_activation, .. } => { + if id == target { + return Ok(on_activation.clone()); + } + + id += 1; + } + menu::Entry::Dropdown { submenu, .. } => { + match find_message(target, id, submenu) { + Ok(message) => { + return Ok(message); + } + Err(n_children) => { + id += n_children; + } + } + } + menu::Entry::Separator => {} + } + } + + Err(id - starting_id) + } + + find_message(id, 0, menu).ok() +} + /// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon. /// /// [`winit`]: https://github.com/rust-windowing/winit @@ -192,12 +357,14 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { pub fn modifiers( modifiers: winit::event::ModifiersState, ) -> keyboard::Modifiers { - keyboard::Modifiers { - shift: modifiers.shift(), - control: modifiers.ctrl(), - alt: modifiers.alt(), - logo: modifiers.logo(), - } + let mut result = keyboard::Modifiers::empty(); + + result.set(keyboard::Modifiers::SHIFT, modifiers.shift()); + result.set(keyboard::Modifiers::CTRL, modifiers.ctrl()); + result.set(keyboard::Modifiers::ALT, modifiers.alt()); + result.set(keyboard::Modifiers::LOGO, modifiers.logo()); + + result } /// Converts a physical cursor position to a logical `Point`. @@ -241,6 +408,183 @@ pub fn touch_event( } } +/// Converts a `KeyCode` from [`iced_native`] to an [`winit`] key code. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +fn to_virtual_keycode( + keycode: keyboard::KeyCode, +) -> winit::event::VirtualKeyCode { + use keyboard::KeyCode; + use winit::event::VirtualKeyCode; + + match keycode { + KeyCode::Key1 => VirtualKeyCode::Key1, + KeyCode::Key2 => VirtualKeyCode::Key2, + KeyCode::Key3 => VirtualKeyCode::Key3, + KeyCode::Key4 => VirtualKeyCode::Key4, + KeyCode::Key5 => VirtualKeyCode::Key5, + KeyCode::Key6 => VirtualKeyCode::Key6, + KeyCode::Key7 => VirtualKeyCode::Key7, + KeyCode::Key8 => VirtualKeyCode::Key8, + KeyCode::Key9 => VirtualKeyCode::Key9, + KeyCode::Key0 => VirtualKeyCode::Key0, + KeyCode::A => VirtualKeyCode::A, + KeyCode::B => VirtualKeyCode::B, + KeyCode::C => VirtualKeyCode::C, + KeyCode::D => VirtualKeyCode::D, + KeyCode::E => VirtualKeyCode::E, + KeyCode::F => VirtualKeyCode::F, + KeyCode::G => VirtualKeyCode::G, + KeyCode::H => VirtualKeyCode::H, + KeyCode::I => VirtualKeyCode::I, + KeyCode::J => VirtualKeyCode::J, + KeyCode::K => VirtualKeyCode::K, + KeyCode::L => VirtualKeyCode::L, + KeyCode::M => VirtualKeyCode::M, + KeyCode::N => VirtualKeyCode::N, + KeyCode::O => VirtualKeyCode::O, + KeyCode::P => VirtualKeyCode::P, + KeyCode::Q => VirtualKeyCode::Q, + KeyCode::R => VirtualKeyCode::R, + KeyCode::S => VirtualKeyCode::S, + KeyCode::T => VirtualKeyCode::T, + KeyCode::U => VirtualKeyCode::U, + KeyCode::V => VirtualKeyCode::V, + KeyCode::W => VirtualKeyCode::W, + KeyCode::X => VirtualKeyCode::X, + KeyCode::Y => VirtualKeyCode::Y, + KeyCode::Z => VirtualKeyCode::Z, + KeyCode::Escape => VirtualKeyCode::Escape, + KeyCode::F1 => VirtualKeyCode::F1, + KeyCode::F2 => VirtualKeyCode::F2, + KeyCode::F3 => VirtualKeyCode::F3, + KeyCode::F4 => VirtualKeyCode::F4, + KeyCode::F5 => VirtualKeyCode::F5, + KeyCode::F6 => VirtualKeyCode::F6, + KeyCode::F7 => VirtualKeyCode::F7, + KeyCode::F8 => VirtualKeyCode::F8, + KeyCode::F9 => VirtualKeyCode::F9, + KeyCode::F10 => VirtualKeyCode::F10, + KeyCode::F11 => VirtualKeyCode::F11, + KeyCode::F12 => VirtualKeyCode::F12, + KeyCode::F13 => VirtualKeyCode::F13, + KeyCode::F14 => VirtualKeyCode::F14, + KeyCode::F15 => VirtualKeyCode::F15, + KeyCode::F16 => VirtualKeyCode::F16, + KeyCode::F17 => VirtualKeyCode::F17, + KeyCode::F18 => VirtualKeyCode::F18, + KeyCode::F19 => VirtualKeyCode::F19, + KeyCode::F20 => VirtualKeyCode::F20, + KeyCode::F21 => VirtualKeyCode::F21, + KeyCode::F22 => VirtualKeyCode::F22, + KeyCode::F23 => VirtualKeyCode::F23, + KeyCode::F24 => VirtualKeyCode::F24, + KeyCode::Snapshot => VirtualKeyCode::Snapshot, + KeyCode::Scroll => VirtualKeyCode::Scroll, + KeyCode::Pause => VirtualKeyCode::Pause, + KeyCode::Insert => VirtualKeyCode::Insert, + KeyCode::Home => VirtualKeyCode::Home, + KeyCode::Delete => VirtualKeyCode::Delete, + KeyCode::End => VirtualKeyCode::End, + KeyCode::PageDown => VirtualKeyCode::PageDown, + KeyCode::PageUp => VirtualKeyCode::PageUp, + KeyCode::Left => VirtualKeyCode::Left, + KeyCode::Up => VirtualKeyCode::Up, + KeyCode::Right => VirtualKeyCode::Right, + KeyCode::Down => VirtualKeyCode::Down, + KeyCode::Backspace => VirtualKeyCode::Back, + KeyCode::Enter => VirtualKeyCode::Return, + KeyCode::Space => VirtualKeyCode::Space, + KeyCode::Compose => VirtualKeyCode::Compose, + KeyCode::Caret => VirtualKeyCode::Caret, + KeyCode::Numlock => VirtualKeyCode::Numlock, + KeyCode::Numpad0 => VirtualKeyCode::Numpad0, + KeyCode::Numpad1 => VirtualKeyCode::Numpad1, + KeyCode::Numpad2 => VirtualKeyCode::Numpad2, + KeyCode::Numpad3 => VirtualKeyCode::Numpad3, + KeyCode::Numpad4 => VirtualKeyCode::Numpad4, + KeyCode::Numpad5 => VirtualKeyCode::Numpad5, + KeyCode::Numpad6 => VirtualKeyCode::Numpad6, + KeyCode::Numpad7 => VirtualKeyCode::Numpad7, + KeyCode::Numpad8 => VirtualKeyCode::Numpad8, + KeyCode::Numpad9 => VirtualKeyCode::Numpad9, + KeyCode::AbntC1 => VirtualKeyCode::AbntC1, + KeyCode::AbntC2 => VirtualKeyCode::AbntC2, + KeyCode::NumpadAdd => VirtualKeyCode::NumpadAdd, + KeyCode::Plus => VirtualKeyCode::Plus, + KeyCode::Apostrophe => VirtualKeyCode::Apostrophe, + KeyCode::Apps => VirtualKeyCode::Apps, + KeyCode::At => VirtualKeyCode::At, + KeyCode::Ax => VirtualKeyCode::Ax, + KeyCode::Backslash => VirtualKeyCode::Backslash, + KeyCode::Calculator => VirtualKeyCode::Calculator, + KeyCode::Capital => VirtualKeyCode::Capital, + KeyCode::Colon => VirtualKeyCode::Colon, + KeyCode::Comma => VirtualKeyCode::Comma, + KeyCode::Convert => VirtualKeyCode::Convert, + KeyCode::NumpadDecimal => VirtualKeyCode::NumpadDecimal, + KeyCode::NumpadDivide => VirtualKeyCode::NumpadDivide, + KeyCode::Equals => VirtualKeyCode::Equals, + KeyCode::Grave => VirtualKeyCode::Grave, + KeyCode::Kana => VirtualKeyCode::Kana, + KeyCode::Kanji => VirtualKeyCode::Kanji, + KeyCode::LAlt => VirtualKeyCode::LAlt, + KeyCode::LBracket => VirtualKeyCode::LBracket, + KeyCode::LControl => VirtualKeyCode::LControl, + KeyCode::LShift => VirtualKeyCode::LShift, + KeyCode::LWin => VirtualKeyCode::LWin, + KeyCode::Mail => VirtualKeyCode::Mail, + KeyCode::MediaSelect => VirtualKeyCode::MediaSelect, + KeyCode::MediaStop => VirtualKeyCode::MediaStop, + KeyCode::Minus => VirtualKeyCode::Minus, + KeyCode::NumpadMultiply => VirtualKeyCode::NumpadMultiply, + KeyCode::Mute => VirtualKeyCode::Mute, + KeyCode::MyComputer => VirtualKeyCode::MyComputer, + KeyCode::NavigateForward => VirtualKeyCode::NavigateForward, + KeyCode::NavigateBackward => VirtualKeyCode::NavigateBackward, + KeyCode::NextTrack => VirtualKeyCode::NextTrack, + KeyCode::NoConvert => VirtualKeyCode::NoConvert, + KeyCode::NumpadComma => VirtualKeyCode::NumpadComma, + KeyCode::NumpadEnter => VirtualKeyCode::NumpadEnter, + KeyCode::NumpadEquals => VirtualKeyCode::NumpadEquals, + KeyCode::OEM102 => VirtualKeyCode::OEM102, + KeyCode::Period => VirtualKeyCode::Period, + KeyCode::PlayPause => VirtualKeyCode::PlayPause, + KeyCode::Power => VirtualKeyCode::Power, + KeyCode::PrevTrack => VirtualKeyCode::PrevTrack, + KeyCode::RAlt => VirtualKeyCode::RAlt, + KeyCode::RBracket => VirtualKeyCode::RBracket, + KeyCode::RControl => VirtualKeyCode::RControl, + KeyCode::RShift => VirtualKeyCode::RShift, + KeyCode::RWin => VirtualKeyCode::RWin, + KeyCode::Semicolon => VirtualKeyCode::Semicolon, + KeyCode::Slash => VirtualKeyCode::Slash, + KeyCode::Sleep => VirtualKeyCode::Sleep, + KeyCode::Stop => VirtualKeyCode::Stop, + KeyCode::NumpadSubtract => VirtualKeyCode::NumpadSubtract, + KeyCode::Sysrq => VirtualKeyCode::Sysrq, + KeyCode::Tab => VirtualKeyCode::Tab, + KeyCode::Underline => VirtualKeyCode::Underline, + KeyCode::Unlabeled => VirtualKeyCode::Unlabeled, + KeyCode::VolumeDown => VirtualKeyCode::VolumeDown, + KeyCode::VolumeUp => VirtualKeyCode::VolumeUp, + KeyCode::Wake => VirtualKeyCode::Wake, + KeyCode::WebBack => VirtualKeyCode::WebBack, + KeyCode::WebFavorites => VirtualKeyCode::WebFavorites, + KeyCode::WebForward => VirtualKeyCode::WebForward, + KeyCode::WebHome => VirtualKeyCode::WebHome, + KeyCode::WebRefresh => VirtualKeyCode::WebRefresh, + KeyCode::WebSearch => VirtualKeyCode::WebSearch, + KeyCode::WebStop => VirtualKeyCode::WebStop, + KeyCode::Yen => VirtualKeyCode::Yen, + KeyCode::Copy => VirtualKeyCode::Copy, + KeyCode::Paste => VirtualKeyCode::Paste, + KeyCode::Cut => VirtualKeyCode::Cut, + KeyCode::Asterisk => VirtualKeyCode::Asterisk, + } +} + /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit diff --git a/winit/src/lib.rs b/winit/src/lib.rs index c9f324dd..1707846a 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -31,12 +31,14 @@ pub mod settings; mod clipboard; mod error; mod mode; +mod position; mod proxy; pub use application::Application; pub use clipboard::Clipboard; pub use error::Error; pub use mode::Mode; +pub use position::Position; pub use proxy::Proxy; pub use settings::Settings; diff --git a/winit/src/mode.rs b/winit/src/mode.rs index 37464711..fdce8e23 100644 --- a/winit/src/mode.rs +++ b/winit/src/mode.rs @@ -6,4 +6,7 @@ pub enum Mode { /// The application takes the whole screen of its current monitor. Fullscreen, + + /// The application is hidden + Hidden, } diff --git a/winit/src/position.rs b/winit/src/position.rs new file mode 100644 index 00000000..c260c29e --- /dev/null +++ b/winit/src/position.rs @@ -0,0 +1,22 @@ +/// The position of a window in a given screen. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The platform-specific default position for a new window. + Default, + /// The window is completely centered on the screen. + Centered, + /// The window is positioned with specific coordinates: `(X, Y)`. + /// + /// When the decorations of the window are enabled, Windows 10 will add some + /// invisible padding to the window. This padding gets included in the + /// position. So if you have decorations enabled and want the window to be + /// at (0, 0) you would have to set the position to + /// `(PADDING_X, PADDING_Y)`. + Specific(i32, i32), +} + +impl Default for Position { + fn default() -> Self { + Self::Default + } +} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 2e8715cd..045cb156 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -2,20 +2,31 @@ #[cfg(target_os = "windows")] #[path = "settings/windows.rs"] mod platform; -#[cfg(not(target_os = "windows"))] -#[path = "settings/not_windows.rs"] + +#[cfg(target_os = "macos")] +#[path = "settings/macos.rs"] +mod platform; + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +#[path = "settings/other.rs"] mod platform; pub use platform::PlatformSpecific; use crate::conversion; -use crate::Mode; +use crate::{Mode, Position}; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; /// The settings of an application. #[derive(Debug, Clone, Default)] pub struct Settings<Flags> { + /// The identifier of the application. + /// + /// If provided, this identifier may be used to identify the application or + /// communicate with it through the windowing system. + pub id: Option<String>, + /// The [`Window`] settings pub window: Window, @@ -23,6 +34,10 @@ pub struct Settings<Flags> { /// /// [`Application`]: crate::Application pub flags: Flags, + + /// Whether the [`Application`] should exit when the user requests the + /// window to close (e.g. the user presses the close button). + pub exit_on_close_request: bool, } /// The window settings of an application. @@ -31,6 +46,9 @@ pub struct Window { /// The size of the window. pub size: (u32, u32), + /// The position of the window. + pub position: Position, + /// The minimum size of the window. pub min_size: Option<(u32, u32)>, @@ -63,6 +81,7 @@ impl Window { title: &str, mode: Mode, primary_monitor: Option<MonitorHandle>, + _id: Option<String>, ) -> WindowBuilder { let mut window_builder = WindowBuilder::new(); @@ -76,7 +95,15 @@ impl Window { .with_transparent(self.transparent) .with_window_icon(self.icon) .with_always_on_top(self.always_on_top) - .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); + .with_visible(conversion::visible(mode)); + + if let Some(position) = conversion::position( + primary_monitor.as_ref(), + self.size, + self.position, + ) { + window_builder = window_builder.with_position(position); + } if let Some((width, height)) = self.min_size { window_builder = window_builder @@ -88,6 +115,21 @@ impl Window { .with_max_inner_size(winit::dpi::LogicalSize { width, height }); } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + use ::winit::platform::unix::WindowBuilderExtUnix; + + if let Some(id) = _id { + window_builder = window_builder.with_app_id(id); + } + } + #[cfg(target_os = "windows")] { use winit::platform::windows::WindowBuilderExtWindows; @@ -95,8 +137,28 @@ impl Window { if let Some(parent) = self.platform_specific.parent { window_builder = window_builder.with_parent_window(parent); } + + window_builder = window_builder + .with_drag_and_drop(self.platform_specific.drag_and_drop); + } + + #[cfg(target_os = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS; + + window_builder = window_builder + .with_title_hidden(self.platform_specific.title_hidden) + .with_titlebar_transparent( + self.platform_specific.titlebar_transparent, + ) + .with_fullsize_content_view( + self.platform_specific.fullsize_content_view, + ); } + window_builder = window_builder + .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); + window_builder } } @@ -105,6 +167,7 @@ impl Default for Window { fn default() -> Window { Window { size: (1024, 768), + position: Position::default(), min_size: None, max_size: None, resizable: true, diff --git a/winit/src/settings/macos.rs b/winit/src/settings/macos.rs new file mode 100644 index 00000000..ad4c8cae --- /dev/null +++ b/winit/src/settings/macos.rs @@ -0,0 +1,13 @@ +#![cfg(target_os = "macos")] +//! Platform specific settings for macOS. + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific { + /// Hides the window title. + pub title_hidden: bool, + /// Makes the titlebar transparent and allows the content to appear behind it. + pub titlebar_transparent: bool, + /// Makes the window content appear behind the titlebar. + pub fullsize_content_view: bool, +} diff --git a/winit/src/settings/not_windows.rs b/winit/src/settings/not_windows.rs deleted file mode 100644 index 5d703f84..00000000 --- a/winit/src/settings/not_windows.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![cfg(not(target_os = "windows"))] -//! Platform specific settings for not Windows. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PlatformSpecific {} diff --git a/winit/src/settings/other.rs b/winit/src/settings/other.rs new file mode 100644 index 00000000..b1103f62 --- /dev/null +++ b/winit/src/settings/other.rs @@ -0,0 +1,3 @@ +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific; diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs index 76b8d067..fc26acd7 100644 --- a/winit/src/settings/windows.rs +++ b/winit/src/settings/windows.rs @@ -2,8 +2,20 @@ //! Platform specific settings for Windows. /// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { - /// Parent Window + /// Parent window pub parent: Option<winapi::shared::windef::HWND>, + + /// Drag and drop support + pub drag_and_drop: bool, +} + +impl Default for PlatformSpecific { + fn default() -> Self { + Self { + parent: None, + drag_and_drop: true, + } + } } |