diff options
author | 2024-01-19 20:41:52 +0100 | |
---|---|---|
committer | 2024-01-19 20:41:52 +0100 | |
commit | 1781068e1c3a65551db1e832fdbaddba99124051 (patch) | |
tree | 60e0b3854cc0541712572fbb0e56f14435951ea9 | |
parent | 41dec5bd203ff5b1574a33a17d5f7358ae1beea2 (diff) | |
parent | 7ae7fcb89855002519bab752fd3686106ce448db (diff) | |
download | iced-1781068e1c3a65551db1e832fdbaddba99124051.tar.gz iced-1781068e1c3a65551db1e832fdbaddba99124051.tar.bz2 iced-1781068e1c3a65551db1e832fdbaddba99124051.zip |
Merge branch 'master' into remove-vertex-indexing
240 files changed, 12774 insertions, 3393 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e02dda8..85a46cda 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,8 +17,6 @@ clippy --workspace --no-deps -- \ -D clippy::useless_conversion """ -#![allow(clippy::inherent_to_string, clippy::type_complexity)] - nitpick = """ clippy --workspace --no-deps -- \ -D warnings \ diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index d4c94fcd..09b31697 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -25,7 +25,6 @@ body: Before filing an issue... - If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples]. - - If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples]. If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly! diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index e9f4b0c5..57169796 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -12,6 +12,8 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - uses: actions/checkout@master + - name: Resolve dependencies + run: cargo update - name: Audit vulnerabilities run: cargo audit @@ -22,5 +24,7 @@ jobs: - name: Install cargo-outdated run: cargo install cargo-outdated - uses: actions/checkout@master + - name: Delete `web-sys` dependency from `integration` example + run: sed -i '$d' examples/integration/Cargo.toml - name: Find outdated dependencies - run: cargo outdated --workspace --exit-code 1 + run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..df9c480f --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,29 @@ +name: Check +on: [push, pull_request] +jobs: + widget: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + - uses: actions/checkout@master + - name: Check standalone `iced_widget` crate + run: cargo check --package iced_widget --features image,svg,canvas + + wasm: + runs-on: ubuntu-latest + env: + RUSTFLAGS: --cfg=web_sys_unstable_apis + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + targets: wasm32-unknown-unknown + - uses: actions/checkout@master + - name: Run checks + run: cargo check --package iced --target wasm32-unknown-unknown + - name: Check compilation of `tour` example + run: cargo build --package tour --target wasm32-unknown-unknown + - name: Check compilation of `todos` example + run: cargo build --package todos --target wasm32-unknown-unknown + - name: Check compilation of `integration` example + run: cargo build --package integration --target wasm32-unknown-unknown diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 230c5cb0..35bf10f4 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -8,13 +8,14 @@ jobs: steps: - uses: hecrj/setup-rust-action@v1 with: - rust-version: nightly + rust-version: nightly-2023-12-11 - uses: actions/checkout@v2 - name: Generate documentation run: | RUSTDOCFLAGS="--cfg docsrs" \ cargo doc --no-deps --all-features \ -p iced_core \ + -p iced_highlighter \ -p iced_style \ -p iced_futures \ -p iced_runtime \ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7fdc8867..2ff86614 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: ubuntu-latest + runs-on: macOS-latest steps: - uses: hecrj/setup-rust-action@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac8d27f9..9c5ee0d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,10 @@ name: Test on: [push, pull_request] jobs: - native: + all: runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: --deny warnings strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] @@ -17,27 +19,8 @@ jobs: run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev + sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Run tests run: | cargo test --verbose --workspace cargo test --verbose --workspace --all-features - - web: - runs-on: ubuntu-latest - env: - RUSTFLAGS: --cfg=web_sys_unstable_apis - steps: - - uses: hecrj/setup-rust-action@v1 - with: - rust-version: stable - targets: wasm32-unknown-unknown - - uses: actions/checkout@master - - name: Run checks - run: cargo check --package iced --target wasm32-unknown-unknown - - name: Check compilation of `tour` example - run: cargo build --package tour --target wasm32-unknown-unknown - - name: Check compilation of `todos` example - run: cargo build --package todos --target wasm32-unknown-unknown - - name: Check compilation of `integration` example - run: cargo build --package integration --target wasm32-unknown-unknown @@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" } [features] default = ["wgpu"] # Enable the `wgpu` GPU-accelerated renderer backend -wgpu = ["iced_renderer/wgpu"] +wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget image = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget @@ -47,6 +47,10 @@ system = ["iced_winit/system"] web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] +# Enables the syntax `highlighter` module +highlighter = ["iced_highlighter"] +# Enables experimental multi-window support. +multi-window = ["iced_winit/multi-window"] # Enables the advanced module advanced = [] @@ -58,6 +62,9 @@ iced_widget.workspace = true iced_winit.features = ["application"] iced_winit.workspace = true +iced_highlighter.workspace = true +iced_highlighter.optional = true + thiserror.workspace = true image.workspace = true @@ -78,8 +85,9 @@ members = [ "core", "futures", "graphics", - "runtime", + "highlighter", "renderer", + "runtime", "style", "tiny_skia", "wgpu", @@ -103,6 +111,7 @@ iced = { version = "0.12", path = "." } iced_core = { version = "0.12", path = "core" } iced_futures = { version = "0.12", path = "futures" } iced_graphics = { version = "0.12", path = "graphics" } +iced_highlighter = { version = "0.12", path = "highlighter" } iced_renderer = { version = "0.12", path = "renderer" } iced_runtime = { version = "0.12", path = "runtime" } iced_style = { version = "0.12", path = "style" } @@ -114,14 +123,13 @@ iced_winit = { version = "0.12", path = "winit" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = "0.9" +cosmic-text = "0.10" futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" } +glyphon = "0.5" guillotiere = "0.6" half = "2.2" image = "0.24" -instant = "0.1" kamadak-exif = "0.5" kurbo = "0.9" log = "0.4" @@ -132,22 +140,25 @@ once_cell = "1.0" ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } -raw-window-handle = "0.5" -resvg = "0.35" +raw-window-handle = "0.6" +resvg = "0.36" rustc-hash = "1.0" smol = "1.0" -softbuffer = "0.2" +smol_str = "0.2" +softbuffer = "0.4" +syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" -tiny-skia = "0.10" +tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" -twox-hash = { version = "1.0", default-features = false } +xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3" -wgpu = "0.17" +web-time = "0.2" +wgpu = "0.19" winapi = "0.3" -window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } +window_clipboard = "0.4" +winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 86581e4a..da3066d8 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua Currently, there are two different official renderers: - [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal. -- [`iced_glow`] is powered by [`glow`] and supports OpenGL 2.1+ and OpenGL ES 2.0+. +- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported. Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate. @@ -54,10 +54,7 @@ The widgets of a graphical user _interface_ are interactive. __Shells__ gather a Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture]. -As of now, there are two official shells: - -- [`iced_winit`] implements a shell runtime on top of [`winit`]. -- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation]. +As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`]. ## The web target The Web platform provides all the abstractions necessary to draw widgets and gather users interactions. @@ -91,5 +88,4 @@ Finally, [`iced`] unifies everything into a simple abstraction to create cross-p [`winit`]: https://github.com/rust-windowing/winit [`glutin`]: https://github.com/rust-windowing/glutin [`dodrio`]: https://github.com/fitzgen/dodrio -[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context [The Elm Architecture]: https://guide.elm-lang.org/architecture/ @@ -9,7 +9,7 @@ [](https://github.com/iced-rs/iced/blob/master/LICENSE) [](https://crates.io/crates/iced) [](https://github.com/iced-rs/iced/actions) -[](https://discourse.iced.rs/) +[](https://discourse.iced.rs/) [](https://discord.gg/3xZJ65GAhd) A cross-platform GUI library for Rust focused on simplicity and type-safety. diff --git a/core/Cargo.toml b/core/Cargo.toml index 7acb7511..32dd3df2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -13,15 +13,17 @@ keywords.workspace = true [dependencies] bitflags.workspace = true log.workspace = true -thiserror.workspace = true -twox-hash.workspace = true num-traits.workspace = true +smol_str.workspace = true +thiserror.workspace = true +web-time.workspace = true +xxhash-rust.workspace = true palette.workspace = true palette.optional = true -[target.'cfg(target_arch = "wasm32")'.dependencies] -instant.workspace = true +[target.'cfg(windows)'.dependencies] +raw-window-handle.workspace = true [dev-dependencies] approx = "0.5" diff --git a/core/src/color.rs b/core/src/color.rs index 0e8b7475..13077628 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,26 @@ impl Color { } } + /// Creates a [`Color`] from its linear RGBA components. + pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + // As described in: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { diff --git a/core/src/element.rs b/core/src/element.rs index dea111af..8b510218 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -6,7 +6,7 @@ use crate::renderer; use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget, + Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, Widget, }; use std::any::Any; @@ -296,12 +296,8 @@ where self.widget.diff(tree); } - fn width(&self) -> Length { - self.widget.width() - } - - fn height(&self) -> Length { - self.widget.height() + fn size(&self) -> Size<Length> { + self.widget.size() } fn layout( @@ -466,12 +462,8 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> where Renderer: crate::Renderer, { - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() + fn size(&self) -> Size<Length> { + self.element.widget.size() } fn tag(&self) -> tree::Tag { diff --git a/core/src/event.rs b/core/src/event.rs index 953cd73f..870b3074 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -19,7 +19,7 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Event), + Window(window::Id, window::Event), /// A touch event Touch(touch::Event), diff --git a/core/src/font.rs b/core/src/font.rs index 7f647847..2b68decf 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -12,8 +12,6 @@ pub struct Font { pub stretch: Stretch, /// The [`Style`] of the [`Font`]. pub style: Style, - /// Whether if the [`Font`] is monospaced or not. - pub monospaced: bool, } impl Font { @@ -23,13 +21,11 @@ impl Font { weight: Weight::Normal, stretch: Stretch::Normal, style: Style::Normal, - monospaced: false, }; /// A monospaced font with normal [`Weight`]. pub const MONOSPACE: Font = Font { family: Family::Monospace, - monospaced: true, ..Self::DEFAULT }; diff --git a/core/src/hasher.rs b/core/src/hasher.rs index 9d8f75b3..a13d78af 100644 --- a/core/src/hasher.rs +++ b/core/src/hasher.rs @@ -1,6 +1,7 @@ /// The hasher used to compare layouts. -#[derive(Debug, Default)] -pub struct Hasher(twox_hash::XxHash64); +#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways. +#[derive(Default)] +pub struct Hasher(xxhash_rust::xxh3::Xxh3); impl core::hash::Hasher for Hasher { fn write(&mut self, bytes: &[u8]) { diff --git a/core/src/image.rs b/core/src/image.rs index 85d9d475..e9675316 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -164,6 +164,16 @@ impl std::fmt::Debug for Data { } } +/// Image filtering strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum FilterMethod { + /// Bilinear interpolation. + #[default] + Linear, + /// Nearest neighbor. + Nearest, +} + /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer @@ -178,5 +188,10 @@ pub trait Renderer: crate::Renderer { /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. - fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); + fn draw( + &mut self, + handle: Self::Handle, + filter_method: FilterMethod, + bounds: Rectangle, + ); } diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index 4c6ca08d..b810ccb0 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,8 +1,11 @@ //! Listen to keyboard events. +pub mod key; + mod event; -mod key_code; +mod location; mod modifiers; pub use event::Event; -pub use key_code::KeyCode; +pub use key::Key; +pub use location::Location; pub use modifiers::Modifiers; diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 016761af..1eb42334 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,4 +1,5 @@ -use super::{KeyCode, Modifiers}; +use crate::keyboard::{Key, Location, Modifiers}; +use crate::SmolStr; /// A keyboard event. /// @@ -6,29 +7,35 @@ use super::{KeyCode, Modifiers}; /// additional events, feel free to [open an issue] and share your use case!_ /// /// [open an issue]: https://github.com/iced-rs/iced/issues -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { /// A keyboard key was pressed. KeyPressed { - /// The key identifier - key_code: KeyCode, + /// The key pressed. + key: Key, - /// The state of the modifier keys + /// The location of the key. + location: Location, + + /// The state of the modifier keys. modifiers: Modifiers, + + /// The text produced by the key press, if any. + text: Option<SmolStr>, }, /// A keyboard key was released. KeyReleased { - /// The key identifier - key_code: KeyCode, + /// The key released. + key: Key, - /// The state of the modifier keys + /// The location of the key. + location: Location, + + /// The state of the modifier keys. modifiers: Modifiers, }, - /// A unicode character was received. - CharacterReceived(char), - /// The keyboard modifiers have changed. ModifiersChanged(Modifiers), } diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs new file mode 100644 index 00000000..dbde5196 --- /dev/null +++ b/core/src/keyboard/key.rs @@ -0,0 +1,744 @@ +//! Identify keyboard keys. +use crate::SmolStr; + +/// A key on the keyboard. +/// +/// This is mostly the `Key` type found in [`winit`]. +/// +/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Key<C = SmolStr> { + /// A key with an established name. + Named(Named), + + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(C), + + /// An unidentified key. + Unidentified, +} + +impl Key { + /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on + /// `Key`. All other variants remain unchanged. + pub fn as_ref(&self) -> Key<&str> { + match self { + Self::Named(named) => Key::Named(*named), + Self::Character(c) => Key::Character(c.as_ref()), + Self::Unidentified => Key::Unidentified, + } + } +} + +/// A named key. +/// +/// This is mostly the `NamedKey` type found in [`winit`]. +/// +/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[allow(missing_docs)] +pub enum Named { + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple <kbd>Option</kbd> key. + Alt, + /// The Alternate Graphics (<kbd>AltGr</kbd> or <kbd>AltGraph</kbd>) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key + /// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for + /// the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for the + /// key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined to + /// produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is + /// typically used to switch to a hiragana keyboard for the purpose of converting input into + /// kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} diff --git a/core/src/keyboard/key_code.rs b/core/src/keyboard/key_code.rs deleted file mode 100644 index 74ead170..00000000 --- a/core/src/keyboard/key_code.rs +++ /dev/null @@ -1,203 +0,0 @@ -/// The symbolic name of a keyboard key. -/// -/// This is mostly the `KeyCode` type found in [`winit`]. -/// -/// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/ -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -#[repr(u32)] -#[allow(missing_docs)] -pub enum KeyCode { - /// The '1' key over the letters. - Key1, - /// The '2' key over the letters. - Key2, - /// The '3' key over the letters. - Key3, - /// The '4' key over the letters. - Key4, - /// The '5' key over the letters. - Key5, - /// The '6' key over the letters. - Key6, - /// The '7' key over the letters. - Key7, - /// The '8' key over the letters. - Key8, - /// The '9' key over the letters. - Key9, - /// The '0' key over the 'O' and 'P' keys. - Key0, - - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - - /// The Escape key, next to F1. - Escape, - - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - - /// Print Screen/SysRq. - Snapshot, - /// Scroll Lock. - Scroll, - /// Pause/Break key, next to Scroll lock. - Pause, - - /// `Insert`, next to Backspace. - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - - Left, - Up, - Right, - Down, - - /// The Backspace key, right over Enter. - Backspace, - /// The Enter key. - Enter, - /// The space bar. - Space, - - /// The "Compose" key on Linux. - Compose, - - Caret, - - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - NumpadAdd, - NumpadDivide, - NumpadDecimal, - NumpadComma, - NumpadEnter, - NumpadEquals, - NumpadMultiply, - NumpadSubtract, - - AbntC1, - AbntC2, - Apostrophe, - Apps, - Asterisk, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - Equals, - Grave, - Kana, - Kanji, - LAlt, - LBracket, - LControl, - LShift, - LWin, - Mail, - MediaSelect, - MediaStop, - Minus, - Mute, - MyComputer, - NavigateForward, // also called "Next" - NavigateBackward, // also called "Prior" - NextTrack, - NoConvert, - OEM102, - Period, - PlayPause, - Plus, - Power, - PrevTrack, - RAlt, - RBracket, - RControl, - RShift, - RWin, - Semicolon, - Slash, - Sleep, - Stop, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, -} diff --git a/core/src/keyboard/location.rs b/core/src/keyboard/location.rs new file mode 100644 index 00000000..feff0820 --- /dev/null +++ b/core/src/keyboard/location.rs @@ -0,0 +1,12 @@ +/// The location of a key on the keyboard. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Location { + /// The standard group of keys on the keyboard. + Standard, + /// The left side of the keyboard. + Left, + /// The right side of the keyboard. + Right, + /// The numpad of the keyboard. + Numpad, +} diff --git a/core/src/layout.rs b/core/src/layout.rs index caf315b6..95720aba 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -7,7 +7,7 @@ pub mod flex; pub use limits::Limits; pub use node::Node; -use crate::{Point, Rectangle, Size, Vector}; +use crate::{Length, Padding, Point, Rectangle, Size, Vector}; /// The bounds of a [`Node`] and its children, using absolute coordinates. #[derive(Debug, Clone, Copy)] @@ -71,12 +71,12 @@ pub fn next_to_each_other( left: impl FnOnce(&Limits) -> Node, right: impl FnOnce(&Limits) -> Node, ) -> Node { - let mut left_node = left(limits); + let left_node = left(limits); let left_size = left_node.size(); let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0)); - let mut right_node = right(&right_limits); + let right_node = right(&right_limits); let right_size = right_node.size(); let (left_y, right_y) = if left_size.height > right_size.height { @@ -85,14 +85,106 @@ pub fn next_to_each_other( ((right_size.height - left_size.height) / 2.0, 0.0) }; - left_node.move_to(Point::new(0.0, left_y)); - right_node.move_to(Point::new(left_size.width + spacing, right_y)); - Node::with_children( Size::new( left_size.width + spacing + right_size.width, left_size.height.max(right_size.height), ), - vec![left_node, right_node], + vec![ + left_node.move_to(Point::new(0.0, left_y)), + right_node.move_to(Point::new(left_size.width + spacing, right_y)), + ], + ) +} + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and no intrinsic size. +pub fn atomic( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, +) -> Node { + let width = width.into(); + let height = height.into(); + + Node::new(limits.resolve(width, height, Size::ZERO)) +} + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and a closure that produces +/// the intrinsic [`Size`] inside the given [`Limits`]. +pub fn sized( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + f: impl FnOnce(&Limits) -> Size, +) -> Node { + let width = width.into(); + let height = height.into(); + + let limits = limits.width(width).height(height); + let intrinsic_size = f(&limits); + + Node::new(limits.resolve(width, height, intrinsic_size)) +} + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and a closure that produces +/// the content [`Node`] inside the given [`Limits`]. +pub fn contained( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + f: impl FnOnce(&Limits) -> Node, +) -> Node { + let width = width.into(); + let height = height.into(); + + let limits = limits.width(width).height(height); + let content = f(&limits); + + Node::with_children( + limits.resolve(width, height, content.size()), + vec![content], + ) +} + +/// Computes the [`Node`] that fits the [`Limits`] given some width, height, and +/// [`Padding`] requirements and a closure that produces the content [`Node`] +/// inside the given [`Limits`]. +pub fn padded( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + padding: impl Into<Padding>, + layout: impl FnOnce(&Limits) -> Node, +) -> Node { + positioned(limits, width, height, padding, layout, |content, _| content) +} + +/// Computes a [`padded`] [`Node`] with a positioning step. +pub fn positioned( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + padding: impl Into<Padding>, + layout: impl FnOnce(&Limits) -> Node, + position: impl FnOnce(Node, Size) -> Node, +) -> Node { + let width = width.into(); + let height = height.into(); + let padding = padding.into(); + + let limits = limits.width(width).height(height); + let content = layout(&limits.shrink(padding)); + let padding = padding.fit(content.size(), limits.max()); + + let size = limits + .shrink(padding) + .resolve(width, height, content.size()); + + Node::with_children( + size.expand(padding), + vec![position(content.move_to((padding.left, padding.top)), size)], ) } diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index c02b63d8..3358ef3d 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -20,7 +20,7 @@ use crate::Element; use crate::layout::{Limits, Node}; use crate::widget; -use crate::{Alignment, Padding, Point, Size}; +use crate::{Alignment, Length, Padding, Point, Size}; /// The main axis of a flex layout. #[derive(Debug)] @@ -47,7 +47,7 @@ impl Axis { } } - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + fn pack<T>(&self, main: T, cross: T) -> (T, T) { match self { Axis::Horizontal => (main, cross), Axis::Vertical => (cross, main), @@ -63,6 +63,8 @@ pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, limits: &Limits, + width: Length, + height: Length, padding: Padding, spacing: f32, align_items: Alignment, @@ -72,26 +74,64 @@ pub fn resolve<Message, Renderer>( where Renderer: crate::Renderer, { - let limits = limits.pad(padding); + let limits = limits.width(width).height(height).shrink(padding); let total_spacing = spacing * items.len().saturating_sub(1) as f32; let max_cross = axis.cross(limits.max()); - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut fill_main_sum = 0; + let mut cross = match axis { + Axis::Horizontal => match height { + Length::Shrink => 0.0, + _ => max_cross, + }, + Axis::Vertical => match width { + Length::Shrink => 0.0, + _ => max_cross, + }, + }; + let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); nodes.resize(items.len(), Node::default()); for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; + + if fill_main_factor == 0 { + if fill_cross_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = + child.as_widget().layout(tree, renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + cross = cross.max(axis.cross(size)); + + nodes[i] = layout; + } + } else { + fill_main_sum += fill_main_factor; } - .fill_factor(); + } - if fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); + for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; + + if fill_main_factor == 0 && fill_cross_factor != 0 { + let (max_width, max_height) = axis.pack(available, cross); let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); @@ -101,34 +141,47 @@ where let size = layout.size(); available -= axis.main(size); - cross = cross.max(axis.cross(size)); + cross = cross.max(axis.cross(layout.size())); nodes[i] = layout; - } else { - fill_sum += fill_factor; } } - let remaining = available.max(0.0); + let remaining = match axis { + Axis::Horizontal => match width { + Length::Shrink => 0.0, + _ => available.max(0.0), + }, + Axis::Vertical => match height { + Length::Shrink => 0.0, + _ => available.max(0.0), + }, + }; for (i, (child, tree)) in items.iter().zip(trees).enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; + + if fill_main_factor != 0 { + let max_main = + remaining * fill_main_factor as f32 / fill_main_sum as f32; - if fill_factor != 0 { - let max_main = remaining * fill_factor as f32 / fill_sum as f32; let min_main = if max_main.is_infinite() { 0.0 } else { max_main }; - let (min_width, min_height) = - axis.pack(min_main, axis.cross(limits.min())); + let max_cross = if fill_cross_factor == 0 { + max_cross + } else { + cross + }; + let (min_width, min_height) = axis.pack(min_main, 0.0); let (max_width, max_height) = axis.pack(max_main, max_cross); let child_limits = Limits::new( @@ -154,18 +207,18 @@ where let (x, y) = axis.pack(main, pad.1); - node.move_to(Point::new(x, y)); + node.move_to_mut(Point::new(x, y)); match axis { Axis::Horizontal => { - node.align( + node.align_mut( Alignment::Start, align_items, Size::new(0.0, cross), ); } Axis::Vertical => { - node.align( + node.align_mut( align_items, Alignment::Start, Size::new(cross, 0.0), @@ -178,8 +231,12 @@ where main += axis.main(size); } - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height)); + let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross); + let size = limits.resolve( + width, + height, + Size::new(intrinsic_width, intrinsic_height), + ); - Node::with_children(size.pad(padding), nodes) + Node::with_children(size.expand(padding), nodes) } diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 5d3c1556..7fbc7b9d 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -1,12 +1,11 @@ #![allow(clippy::manual_clamp)] -use crate::{Length, Padding, Size}; +use crate::{Length, Size}; /// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Limits { min: Size, max: Size, - fill: Size, } impl Limits { @@ -14,16 +13,11 @@ impl Limits { pub const NONE: Limits = Limits { min: Size::ZERO, max: Size::INFINITY, - fill: Size::INFINITY, }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. pub const fn new(min: Size, max: Size) -> Limits { - Limits { - min, - max, - fill: Size::INFINITY, - } + Limits { min, max } } /// Returns the minimum [`Size`] of the [`Limits`]. @@ -36,26 +30,15 @@ impl Limits { self.max } - /// Returns the fill [`Size`] of the [`Limits`]. - pub fn fill(&self) -> Size { - self.fill - } - /// Applies a width constraint to the current [`Limits`]. pub fn width(mut self, width: impl Into<Length>) -> Limits { match width.into() { - Length::Shrink => { - self.fill.width = self.min.width; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.width = self.fill.width.min(self.max.width); - } + Length::Shrink | Length::Fill | Length::FillPortion(_) => {} Length::Fixed(amount) => { let new_width = amount.min(self.max.width).max(self.min.width); self.min.width = new_width; self.max.width = new_width; - self.fill.width = new_width; } } @@ -65,19 +48,13 @@ impl Limits { /// Applies a height constraint to the current [`Limits`]. pub fn height(mut self, height: impl Into<Length>) -> Limits { match height.into() { - Length::Shrink => { - self.fill.height = self.min.height; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.height = self.fill.height.min(self.max.height); - } + Length::Shrink | Length::Fill | Length::FillPortion(_) => {} Length::Fixed(amount) => { let new_height = amount.min(self.max.height).max(self.min.height); self.min.height = new_height; self.max.height = new_height; - self.fill.height = new_height; } } @@ -112,13 +89,10 @@ impl Limits { self } - /// Shrinks the current [`Limits`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Limits { - self.shrink(Size::new(padding.horizontal(), padding.vertical())) - } - /// Shrinks the current [`Limits`] by the given [`Size`]. - pub fn shrink(&self, size: Size) -> Limits { + pub fn shrink(&self, size: impl Into<Size>) -> Limits { + let size = size.into(); + let min = Size::new( (self.min().width - size.width).max(0.0), (self.min().height - size.height).max(0.0), @@ -129,12 +103,7 @@ impl Limits { (self.max().height - size.height).max(0.0), ); - let fill = Size::new( - (self.fill.width - size.width).max(0.0), - (self.fill.height - size.height).max(0.0), - ); - - Limits { min, max, fill } + Limits { min, max } } /// Removes the minimum width constraint for the current [`Limits`]. @@ -142,22 +111,39 @@ impl Limits { Limits { min: Size::ZERO, max: self.max, - fill: self.fill, } } - /// Computes the resulting [`Size`] that fits the [`Limits`] given the - /// intrinsic size of some content. - pub fn resolve(&self, intrinsic_size: Size) -> Size { - Size::new( - intrinsic_size - .width - .min(self.max.width) - .max(self.fill.width), - intrinsic_size + /// Computes the resulting [`Size`] that fits the [`Limits`] given + /// some width and height requirements and the intrinsic size of + /// some content. + pub fn resolve( + &self, + width: impl Into<Length>, + height: impl Into<Length>, + intrinsic_size: Size, + ) -> Size { + let width = match width.into() { + Length::Fill | Length::FillPortion(_) => self.max.width, + Length::Fixed(amount) => { + amount.min(self.max.width).max(self.min.width) + } + Length::Shrink => { + intrinsic_size.width.min(self.max.width).max(self.min.width) + } + }; + + let height = match height.into() { + Length::Fill | Length::FillPortion(_) => self.max.height, + Length::Fixed(amount) => { + amount.min(self.max.height).max(self.min.height) + } + Length::Shrink => intrinsic_size .height .min(self.max.height) - .max(self.fill.height), - ) + .max(self.min.height), + }; + + Size::new(width, height) } } diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 2b44a7d5..5743a9bd 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -1,4 +1,4 @@ -use crate::{Alignment, Point, Rectangle, Size, Vector}; +use crate::{Alignment, Padding, Point, Rectangle, Size, Vector}; /// The bounds of an element and its children. #[derive(Debug, Clone, Default)] @@ -26,6 +26,14 @@ impl Node { } } + /// Creates a new [`Node`] that wraps a single child with some [`Padding`]. + pub fn container(child: Self, padding: Padding) -> Self { + Self::with_children( + child.bounds.size().expand(padding), + vec![child.move_to(Point::new(padding.left, padding.top))], + ) + } + /// Returns the [`Size`] of the [`Node`]. pub fn size(&self) -> Size { Size::new(self.bounds.width, self.bounds.height) @@ -43,6 +51,17 @@ impl Node { /// Aligns the [`Node`] in the given space. pub fn align( + mut self, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, + space: Size, + ) -> Self { + self.align_mut(horizontal_alignment, vertical_alignment, space); + self + } + + /// Mutable reference version of [`Self::align`]. + pub fn align_mut( &mut self, horizontal_alignment: Alignment, vertical_alignment: Alignment, @@ -70,13 +89,23 @@ impl Node { } /// Moves the [`Node`] to the given position. - pub fn move_to(&mut self, position: Point) { + pub fn move_to(mut self, position: impl Into<Point>) -> Self { + self.move_to_mut(position); + self + } + + /// Mutable reference version of [`Self::move_to`]. + pub fn move_to_mut(&mut self, position: impl Into<Point>) { + let position = position.into(); + self.bounds.x = position.x; self.bounds.y = position.y; } /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: Vector) -> Self { + pub fn translate(self, translation: impl Into<Vector>) -> Self { + let translation = translation.into(); + Self { bounds: self.bounds + translation, ..self diff --git a/core/src/length.rs b/core/src/length.rs index 3adb996e..4c139895 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -36,6 +36,24 @@ impl Length { Length::Fixed(_) => 0, } } + + /// Returns `true` iff the [`Length`] is either [`Length::Fill`] or + // [`Length::FillPortion`]. + pub fn is_fill(&self) -> bool { + self.fill_factor() != 0 + } + + /// Returns the "fluid" variant of the [`Length`]. + /// + /// Specifically: + /// - [`Length::Shrink`] if [`Length::Shrink`] or [`Length::Fixed`]. + /// - [`Length::Fill`] otherwise. + pub fn fluid(&self) -> Length { + match self { + Length::Fill | Length::FillPortion(_) => Length::Fill, + Length::Shrink | Length::Fixed(_) => Length::Shrink, + } + } } impl From<Pixels> for Length { diff --git a/core/src/lib.rs b/core/src/lib.rs index 54ea5839..864df6e6 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -75,3 +75,5 @@ pub use size::Size; pub use text::Text; pub use vector::Vector; pub use widget::Widget; + +pub use smol_str::SmolStr; diff --git a/core/src/mouse/button.rs b/core/src/mouse/button.rs index 3eec7f42..a8f90329 100644 --- a/core/src/mouse/button.rs +++ b/core/src/mouse/button.rs @@ -10,6 +10,12 @@ pub enum Button { /// The middle (wheel) button. Middle, + /// The back mouse button. + Back, + + /// The forward mouse button. + Forward, + /// Some other button. Other(u16), } diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 9cc44a71..6f3844be 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,11 @@ impl Click { self.kind } + /// Returns the position of the [`Click`]. + pub fn position(&self) -> Point { + self.position + } + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { let duration = if time > self.time { Some(time - self.time) diff --git a/core/src/overlay.rs b/core/src/overlay.rs index f71f25f7..af10afee 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -11,7 +11,7 @@ use crate::mouse; use crate::renderer; use crate::widget; use crate::widget::Tree; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; /// An interactive component that can be displayed on top of other widgets. pub trait Overlay<Message, Renderer> @@ -29,6 +29,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node; /// Draws the [`Overlay`] using the associated `Renderer`. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 3dd58f9b..a279fe28 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -13,6 +13,7 @@ use std::any::Any; #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { position: Point, + translation: Vector, overlay: Box<dyn Overlay<Message, Renderer> + 'a>, } @@ -25,7 +26,11 @@ where position: Point, overlay: Box<dyn Overlay<Message, Renderer> + 'a>, ) -> Self { - Self { position, overlay } + Self { + position, + overlay, + translation: Vector::ZERO, + } } /// Returns the position of the [`Element`]. @@ -36,6 +41,7 @@ where /// Translates the [`Element`]. pub fn translate(mut self, translation: Vector) -> Self { self.position = self.position + translation; + self.translation = self.translation + translation; self } @@ -48,6 +54,7 @@ where { Element { position: self.position, + translation: self.translation, overlay: Box::new(Map::new(self.overlay, f)), } } @@ -59,8 +66,12 @@ where bounds: Size, translation: Vector, ) -> layout::Node { - self.overlay - .layout(renderer, bounds, self.position + translation) + self.overlay.layout( + renderer, + bounds, + self.position + translation, + self.translation + translation, + ) } /// Processes a runtime [`Event`]. @@ -154,8 +165,9 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { - self.content.layout(renderer, bounds, position) + self.content.layout(renderer, bounds, position, translation) } fn operate( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index dccf6dba..e1e9727a 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,7 +4,9 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; +use crate::{ + Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector, +}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// children. @@ -64,10 +66,9 @@ where &mut self, renderer: &Renderer, bounds: Size, - position: Point, + _position: Point, + translation: Vector, ) -> layout::Node { - let translation = position - Point::ORIGIN; - layout::Node::with_children( bounds, self.children diff --git a/core/src/padding.rs b/core/src/padding.rs index 0b1bba13..a63f6e29 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -154,3 +154,9 @@ impl From<[f32; 4]> for Padding { } } } + +impl From<Padding> for Size { + fn from(padding: Padding) -> Self { + Self::new(padding.horizontal(), padding.vertical()) + } +} diff --git a/core/src/point.rs b/core/src/point.rs index 9bf7726b..cea57518 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -1,26 +1,34 @@ use crate::Vector; +use num_traits::{Float, Num}; +use std::fmt; + /// A 2D point. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Point { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Point<T = f32> { /// The X coordinate. - pub x: f32, + pub x: T, /// The Y coordinate. - pub y: f32, + pub y: T, } impl Point { /// The origin (i.e. a [`Point`] at (0, 0)). - pub const ORIGIN: Point = Point::new(0.0, 0.0); + pub const ORIGIN: Self = Self::new(0.0, 0.0); +} +impl<T: Num> Point<T> { /// Creates a new [`Point`] with the given coordinates. - pub const fn new(x: f32, y: f32) -> Self { + pub const fn new(x: T, y: T) -> Self { Self { x, y } } /// Computes the distance to another [`Point`]. - pub fn distance(&self, to: Point) -> f32 { + pub fn distance(&self, to: Self) -> T + where + T: Float, + { let a = self.x - to.x; let b = self.y - to.y; @@ -28,28 +36,37 @@ impl Point { } } -impl From<[f32; 2]> for Point { - fn from([x, y]: [f32; 2]) -> Self { +impl<T> From<[T; 2]> for Point<T> +where + T: Num, +{ + fn from([x, y]: [T; 2]) -> Self { Point { x, y } } } -impl From<[u16; 2]> for Point { - fn from([x, y]: [u16; 2]) -> Self { - Point::new(x.into(), y.into()) +impl<T> From<(T, T)> for Point<T> +where + T: Num, +{ + fn from((x, y): (T, T)) -> Self { + Self { x, y } } } -impl From<Point> for [f32; 2] { - fn from(point: Point) -> [f32; 2] { +impl<T> From<Point<T>> for [T; 2] { + fn from(point: Point<T>) -> [T; 2] { [point.x, point.y] } } -impl std::ops::Add<Vector> for Point { +impl<T> std::ops::Add<Vector<T>> for Point<T> +where + T: std::ops::Add<Output = T>, +{ type Output = Self; - fn add(self, vector: Vector) -> Self { + fn add(self, vector: Vector<T>) -> Self { Self { x: self.x + vector.x, y: self.y + vector.y, @@ -57,10 +74,13 @@ impl std::ops::Add<Vector> for Point { } } -impl std::ops::Sub<Vector> for Point { +impl<T> std::ops::Sub<Vector<T>> for Point<T> +where + T: std::ops::Sub<Output = T>, +{ type Output = Self; - fn sub(self, vector: Vector) -> Self { + fn sub(self, vector: Vector<T>) -> Self { Self { x: self.x - vector.x, y: self.y - vector.y, @@ -68,10 +88,22 @@ impl std::ops::Sub<Vector> for Point { } } -impl std::ops::Sub<Point> for Point { - type Output = Vector; +impl<T> std::ops::Sub<Point<T>> for Point<T> +where + T: std::ops::Sub<Output = T>, +{ + type Output = Vector<T>; - fn sub(self, point: Point) -> Vector { + fn sub(self, point: Self) -> Vector<T> { Vector::new(self.x - point.x, self.y - point.y) } } + +impl<T> fmt::Display for Point<T> +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y) + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 55d58a59..7accd34e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -43,6 +43,7 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; type Paragraph = (); + type Editor = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -58,21 +59,21 @@ impl text::Renderer for Null { fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph { - } - - fn resize_paragraph( - &self, - _paragraph: &mut Self::Paragraph, - _new_bounds: Size, + fn fill_paragraph( + &mut self, + _paragraph: &Self::Paragraph, + _position: Point, + _color: Color, + _clip_bounds: Rectangle, ) { } - fn fill_paragraph( + fn fill_editor( &mut self, - _paragraph: &Self::Paragraph, + _editor: &Self::Editor, _position: Point, _color: Color, + _clip_bounds: Rectangle, ) { } @@ -81,6 +82,7 @@ impl text::Renderer for Null { _paragraph: Text<'_, Self::Font>, _position: Point, _color: Color, + _clip_bounds: Rectangle, ) { } } @@ -88,47 +90,83 @@ impl text::Renderer for Null { impl text::Paragraph for () { type Font = Font; - fn content(&self) -> &str { - "" + fn with_text(_text: Text<'_, Self::Font>) -> Self {} + + fn resize(&mut self, _new_bounds: Size) {} + + fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference { + text::Difference::None } - fn text_size(&self) -> Pixels { - Pixels(16.0) + fn horizontal_alignment(&self) -> alignment::Horizontal { + alignment::Horizontal::Left } - fn font(&self) -> Self::Font { - Font::default() + fn vertical_alignment(&self) -> alignment::Vertical { + alignment::Vertical::Top } - fn line_height(&self) -> text::LineHeight { - text::LineHeight::default() + fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> { + None } - fn shaping(&self) -> text::Shaping { - text::Shaping::default() + fn min_bounds(&self) -> Size { + Size::ZERO } - fn horizontal_alignment(&self) -> alignment::Horizontal { - alignment::Horizontal::Left + fn hit_test(&self, _point: Point) -> Option<text::Hit> { + None } +} - fn vertical_alignment(&self) -> alignment::Vertical { - alignment::Vertical::Top +impl text::Editor for () { + type Font = Font; + + fn with_text(_text: &str) -> Self {} + + fn cursor(&self) -> text::editor::Cursor { + text::editor::Cursor::Caret(Point::ORIGIN) } - fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> { + fn cursor_position(&self) -> (usize, usize) { + (0, 0) + } + + fn selection(&self) -> Option<String> { + None + } + + fn line(&self, _index: usize) -> Option<&str> { None } + fn line_count(&self) -> usize { + 0 + } + + fn perform(&mut self, _action: text::editor::Action) {} + fn bounds(&self) -> Size { Size::ZERO } - fn min_bounds(&self) -> Size { - Size::ZERO + fn update( + &mut self, + _new_bounds: Size, + _new_font: Self::Font, + _new_size: Pixels, + _new_line_height: text::LineHeight, + _new_highlighter: &mut impl text::Highlighter, + ) { } - fn hit_test(&self, _point: Point) -> Option<text::Hit> { - None + fn highlight<H: text::Highlighter>( + &mut self, + _font: Self::Font, + _highlighter: &mut H, + _format_highlight: impl Fn( + &H::Highlight, + ) -> text::highlighter::Format<Self::Font>, + ) { } } diff --git a/core/src/size.rs b/core/src/size.rs index 7ef2f602..90e50d13 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,4 +1,4 @@ -use crate::{Padding, Vector}; +use crate::Vector; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -26,15 +26,7 @@ impl Size { /// A [`Size`] with infinite width and height. pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); - /// Increments the [`Size`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Self { - Size { - width: self.width + padding.horizontal(), - height: self.height + padding.vertical(), - } - } - - /// Returns the minimum of each component of this size and another + /// Returns the minimum of each component of this size and another. pub fn min(self, other: Self) -> Self { Size { width: self.width.min(other.width), @@ -42,13 +34,23 @@ impl Size { } } - /// Returns the maximum of each component of this size and another + /// Returns the maximum of each component of this size and another. pub fn max(self, other: Self) -> Self { Size { width: self.width.max(other.width), height: self.height.max(other.height), } } + + /// Expands this [`Size`] by the given amount. + pub fn expand(self, other: impl Into<Size>) -> Self { + let other = other.into(); + + Size { + width: self.width + other.width, + height: self.height + other.height, + } + } } impl From<[f32; 2]> for Size { diff --git a/core/src/text.rs b/core/src/text.rs index 0e3617b1..edef79c2 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,6 +1,15 @@ //! Draw and interact with text. +mod paragraph; + +pub mod editor; +pub mod highlighter; + +pub use editor::Editor; +pub use highlighter::Highlighter; +pub use paragraph::Paragraph; + use crate::alignment; -use crate::{Color, Pixels, Point, Size}; +use crate::{Color, Pixels, Point, Rectangle, Size}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -126,6 +135,33 @@ impl Hit { } } +/// The difference detected in some text. +/// +/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some +/// [`Text`]. +/// +/// [`compare`]: Paragraph::compare +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Difference { + /// No difference. + /// + /// The text can be reused as it is! + None, + + /// A bounds difference. + /// + /// This normally means a relayout is necessary, but the shape of the text can + /// be reused. + Bounds, + + /// A shape difference. + /// + /// The contents, alignment, sizes, fonts, or any other essential attributes + /// of the shape of the text have changed. A complete reshape and relayout of + /// the text is necessary. + Shape, +} + /// A renderer capable of measuring and drawing [`Text`]. pub trait Renderer: crate::Renderer { /// The font type used. @@ -134,6 +170,9 @@ pub trait Renderer: crate::Renderer { /// The [`Paragraph`] of this [`Renderer`]. type Paragraph: Paragraph<Font = Self::Font> + 'static; + /// The [`Editor`] of this [`Renderer`]. + type Editor: Editor<Font = Self::Font> + 'static; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -156,33 +195,6 @@ pub trait Renderer: crate::Renderer { /// Loads a [`Self::Font`] from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph; - - /// Lays out the given [`Paragraph`] with some new boundaries. - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ); - - /// Updates a [`Paragraph`] to match the given [`Text`], if needed. - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - match compare(paragraph, text) { - Difference::None => {} - Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( @@ -190,6 +202,17 @@ pub trait Renderer: crate::Renderer { text: &Self::Paragraph, position: Point, color: Color, + clip_bounds: Rectangle, + ); + + /// Draws the given [`Editor`] at the given position and with the given + /// [`Color`]. + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, ); /// Draws the given [`Text`] at the given position and with the given @@ -199,103 +222,6 @@ pub trait Renderer: crate::Renderer { text: Text<'_, Self::Font>, position: Point, color: Color, + clip_bounds: Rectangle, ); } -/// A text paragraph. -pub trait Paragraph: Default { - /// The font of this [`Paragraph`]. - type Font; - - /// Returns the content of the [`Paragraph`]. - fn content(&self) -> &str; - - /// Returns the text size of the [`Paragraph`]. - fn text_size(&self) -> Pixels; - - /// Returns the [`LineHeight`] of the [`Paragraph`]. - fn line_height(&self) -> LineHeight; - - /// Returns the [`Self::Font`] of the [`Paragraph`]. - fn font(&self) -> Self::Font; - - /// Returns the [`Shaping`] strategy of the [`Paragraph`]. - fn shaping(&self) -> Shaping; - - /// Returns the horizontal alignment of the [`Paragraph`]. - fn horizontal_alignment(&self) -> alignment::Horizontal; - - /// Returns the vertical alignment of the [`Paragraph`]. - fn vertical_alignment(&self) -> alignment::Vertical; - - /// Returns the boundaries of the [`Paragraph`]. - fn bounds(&self) -> Size; - - /// Returns the minimum boundaries that can fit the contents of the - /// [`Paragraph`]. - fn min_bounds(&self) -> Size; - - /// Tests whether the provided point is within the boundaries of the - /// [`Paragraph`], returning information about the nearest character. - fn hit_test(&self, point: Point) -> Option<Hit>; - - /// Returns the distance to the given grapheme index in the [`Paragraph`]. - fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; - - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Paragraph`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } -} - -/// The difference detected in some text. -/// -/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some -/// [`Text`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Difference { - /// No difference. - /// - /// The text can be reused as it is! - None, - - /// A bounds difference. - /// - /// This normally means a relayout is necessary, but the shape of the text can - /// be reused. - Bounds, - - /// A shape difference. - /// - /// The contents, alignment, sizes, fonts, or any other essential attributes - /// of the shape of the text have changed. A complete reshape and relayout of - /// the text is necessary. - Shape, -} - -/// Compares a [`Paragraph`] with some desired [`Text`] and returns the -/// [`Difference`]. -pub fn compare<Font: PartialEq>( - paragraph: &impl Paragraph<Font = Font>, - text: Text<'_, Font>, -) -> Difference { - if paragraph.content() != text.content - || paragraph.text_size() != text.size - || paragraph.line_height().to_absolute(text.size) - != text.line_height.to_absolute(text.size) - || paragraph.font() != text.font - || paragraph.shaping() != text.shaping - || paragraph.horizontal_alignment() != text.horizontal_alignment - || paragraph.vertical_alignment() != text.vertical_alignment - { - Difference::Shape - } else if paragraph.bounds() != text.bounds { - Difference::Bounds - } else { - Difference::None - } -} diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs new file mode 100644 index 00000000..f3c6e342 --- /dev/null +++ b/core/src/text/editor.rs @@ -0,0 +1,181 @@ +//! Edit text. +use crate::text::highlighter::{self, Highlighter}; +use crate::text::LineHeight; +use crate::{Pixels, Point, Rectangle, Size}; + +use std::sync::Arc; + +/// A component that can be used by widgets to edit multi-line text. +pub trait Editor: Sized + Default { + /// The font of the [`Editor`]. + type Font: Copy + PartialEq + Default; + + /// Creates a new [`Editor`] laid out with the given text. + fn with_text(text: &str) -> Self; + + /// Returns the current [`Cursor`] of the [`Editor`]. + fn cursor(&self) -> Cursor; + + /// Returns the current cursor position of the [`Editor`]. + /// + /// Line and column, respectively. + fn cursor_position(&self) -> (usize, usize); + + /// Returns the current selected text of the [`Editor`]. + fn selection(&self) -> Option<String>; + + /// Returns the text of the given line in the [`Editor`], if it exists. + fn line(&self, index: usize) -> Option<&str>; + + /// Returns the amount of lines in the [`Editor`]. + fn line_count(&self) -> usize; + + /// Performs an [`Action`] on the [`Editor`]. + fn perform(&mut self, action: Action); + + /// Returns the current boundaries of the [`Editor`]. + fn bounds(&self) -> Size; + + /// Updates the [`Editor`] with some new attributes. + fn update( + &mut self, + new_bounds: Size, + new_font: Self::Font, + new_size: Pixels, + new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, + ); + + /// Runs a text [`Highlighter`] in the [`Editor`]. + fn highlight<H: Highlighter>( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, + ); +} + +/// An interaction with an [`Editor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Action { + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Perform an [`Edit`]. + Edit(Edit), + /// Click the [`Editor`] at the given [`Point`]. + Click(Point), + /// Drag the mouse on the [`Editor`] to the given [`Point`]. + Drag(Point), + /// Scroll the [`Editor`] a certain amount of lines. + Scroll { + /// The amount of lines to scroll. + lines: i32, + }, +} + +impl Action { + /// Returns whether the [`Action`] is an editing action. + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(_)) + } +} + +/// An action that edits text. +#[derive(Debug, Clone, PartialEq)] +pub enum Edit { + /// Insert the given character. + Insert(char), + /// Paste the given text. + Paste(Arc<String>), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, +} + +/// A cursor movement. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Motion { + /// Move left. + Left, + /// Move right. + Right, + /// Move up. + Up, + /// Move down. + Down, + /// Move to the left boundary of a word. + WordLeft, + /// Move to the right boundary of a word. + WordRight, + /// Move to the start of the line. + Home, + /// Move to the end of the line. + End, + /// Move to the start of the previous window. + PageUp, + /// Move to the start of the next window. + PageDown, + /// Move to the start of the text. + DocumentStart, + /// Move to the end of the text. + DocumentEnd, +} + +impl Motion { + /// Widens the [`Motion`], if possible. + pub fn widen(self) -> Self { + match self { + Self::Left => Self::WordLeft, + Self::Right => Self::WordRight, + Self::Home => Self::DocumentStart, + Self::End => Self::DocumentEnd, + _ => self, + } + } + + /// Returns the [`Direction`] of the [`Motion`]. + pub fn direction(&self) -> Direction { + match self { + Self::Left + | Self::Up + | Self::WordLeft + | Self::Home + | Self::PageUp + | Self::DocumentStart => Direction::Left, + Self::Right + | Self::Down + | Self::WordRight + | Self::End + | Self::PageDown + | Self::DocumentEnd => Direction::Right, + } + } +} + +/// A direction in some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + /// <- + Left, + /// -> + Right, +} + +/// The cursor of an [`Editor`]. +#[derive(Debug, Clone)] +pub enum Cursor { + /// Cursor without a selection + Caret(Point), + + /// Cursor selecting a range of text + Selection(Vec<Rectangle>), +} diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs new file mode 100644 index 00000000..a0535228 --- /dev/null +++ b/core/src/text/highlighter.rs @@ -0,0 +1,88 @@ +//! Highlight text. +use crate::Color; + +use std::ops::Range; + +/// A type capable of highlighting text. +/// +/// A [`Highlighter`] highlights lines in sequence. When a line changes, +/// it must be notified and the lines after the changed one must be fed +/// again to the [`Highlighter`]. +pub trait Highlighter: 'static { + /// The settings to configure the [`Highlighter`]. + type Settings: PartialEq + Clone; + + /// The output of the [`Highlighter`]. + type Highlight; + + /// The highlight iterator type. + type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)> + where + Self: 'a; + + /// Creates a new [`Highlighter`] from its [`Self::Settings`]. + fn new(settings: &Self::Settings) -> Self; + + /// Updates the [`Highlighter`] with some new [`Self::Settings`]. + fn update(&mut self, new_settings: &Self::Settings); + + /// Notifies the [`Highlighter`] that the line at the given index has changed. + fn change_line(&mut self, line: usize); + + /// Highlights the given line. + /// + /// If a line changed prior to this, the first line provided here will be the + /// line that changed. + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + + /// Returns the current line of the [`Highlighter`]. + /// + /// If `change_line` has been called, this will normally be the least index + /// that changed. + fn current_line(&self) -> usize; +} + +/// A highlighter that highlights nothing. +#[derive(Debug, Clone, Copy)] +pub struct PlainText; + +impl Highlighter for PlainText { + type Settings = (); + type Highlight = (); + + type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>; + + fn new(_settings: &Self::Settings) -> Self { + Self + } + + fn update(&mut self, _new_settings: &Self::Settings) {} + + fn change_line(&mut self, _line: usize) {} + + fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { + std::iter::empty() + } + + fn current_line(&self) -> usize { + usize::MAX + } +} + +/// The format of some text. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Format<Font> { + /// The [`Color`] of the text. + pub color: Option<Color>, + /// The `Font` of the text. + pub font: Option<Font>, +} + +impl<Font> Default for Format<Font> { + fn default() -> Self { + Self { + color: None, + font: None, + } + } +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs new file mode 100644 index 00000000..de1fb74d --- /dev/null +++ b/core/src/text/paragraph.rs @@ -0,0 +1,59 @@ +use crate::alignment; +use crate::text::{Difference, Hit, Text}; +use crate::{Point, Size}; + +/// A text paragraph. +pub trait Paragraph: Sized + Default { + /// The font of this [`Paragraph`]. + type Font: Copy + PartialEq; + + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; + + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); + + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; + + /// Returns the horizontal alignment of the [`Paragraph`]. + fn horizontal_alignment(&self) -> alignment::Horizontal; + + /// Returns the vertical alignment of the [`Paragraph`]. + fn vertical_alignment(&self) -> alignment::Vertical; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + fn min_bounds(&self) -> Size; + + /// Tests whether the provided point is within the boundaries of the + /// [`Paragraph`], returning information about the nearest character. + fn hit_test(&self, point: Point) -> Option<Hit>; + + /// Returns the distance to the given grapheme index in the [`Paragraph`]. + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; + + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Paragraph`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} diff --git a/core/src/time.rs b/core/src/time.rs index 9355ae6d..dcfe4e41 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -1,13 +1,4 @@ //! Keep track of time, both in native and web platforms! -#[cfg(target_arch = "wasm32")] -pub use instant::Instant; - -#[cfg(target_arch = "wasm32")] -pub use instant::Duration; - -#[cfg(not(target_arch = "wasm32"))] -pub use std::time::Instant; - -#[cfg(not(target_arch = "wasm32"))] -pub use std::time::Duration; +pub use web_time::Duration; +pub use web_time::Instant; diff --git a/core/src/widget.rs b/core/src/widget.rs index 294d5984..7f5632ae 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -15,7 +15,7 @@ use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{Clipboard, Length, Rectangle, Shell}; +use crate::{Clipboard, Length, Rectangle, Shell, Size}; /// A component that displays information and allows interaction. /// @@ -43,11 +43,16 @@ pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, { - /// Returns the width of the [`Widget`]. - fn width(&self) -> Length; + /// Returns the [`Size`] of the [`Widget`] in lengths. + fn size(&self) -> Size<Length>; - /// Returns the height of the [`Widget`]. - fn height(&self) -> Length; + /// Returns a [`Size`] hint for laying out the [`Widget`]. + /// + /// This hint may be used by some widget containers to adjust their sizing strategy + /// during construction. + fn size_hint(&self) -> Size<Length> { + self.size() + } /// Returns the [`layout::Node`] of the [`Widget`]. /// diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index ba98f2d8..4cabc7ce 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -5,7 +5,9 @@ use crate::mouse; use crate::renderer; use crate::text::{self, Paragraph}; use crate::widget::tree::{self, Tree}; -use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget}; +use crate::{ + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, +}; use std::borrow::Cow; @@ -134,12 +136,11 @@ where tree::State::new(State(Renderer::Paragraph::default())) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -172,7 +173,7 @@ where style: &renderer::Style, layout: Layout<'_>, _cursor_position: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); @@ -182,6 +183,7 @@ where layout, state, theme.appearance(self.style.clone()), + viewport, ); } } @@ -204,17 +206,15 @@ pub fn layout<Renderer>( where Renderer: text::Renderer, { - let limits = limits.width(width).height(height); - let bounds = limits.max(); + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); - let size = size.unwrap_or_else(|| renderer.default_size()); - let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); - let State(ref mut paragraph) = state; + let State(ref mut paragraph) = state; - renderer.update_paragraph( - paragraph, - text::Text { + paragraph.update(text::Text { content, bounds, size, @@ -223,12 +223,10 @@ where horizontal_alignment, vertical_alignment, shaping, - }, - ); - - let size = limits.resolve(paragraph.min_bounds()); + }); - layout::Node::new(size) + paragraph.min_bounds() + }) } /// Draws text using the same logic as the [`Text`] widget. @@ -247,6 +245,7 @@ pub fn draw<Renderer>( layout: Layout<'_>, state: &State<Renderer::Paragraph>, appearance: Appearance, + viewport: &Rectangle, ) where Renderer: text::Renderer, { @@ -269,6 +268,7 @@ pub fn draw<Renderer>( paragraph, Point::new(x, y), appearance.color.unwrap_or(style.text_color), + *viewport, ); } diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index d4b8828a..ff52b1ce 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -67,7 +67,7 @@ impl Tree { } } - /// Reconciliates the children of the tree with the provided list of widgets. + /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Renderer>( &mut self, new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>], diff --git a/core/src/window.rs b/core/src/window.rs index a6dbdfb4..448ffc45 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -1,15 +1,21 @@ //! Build window-based GUI applications. pub mod icon; +pub mod settings; mod event; +mod id; mod level; mod mode; +mod position; mod redraw_request; mod user_attention; pub use event::Event; pub use icon::Icon; +pub use id::Id; pub use level::Level; pub use mode::Mode; +pub use position::Position; pub use redraw_request::RedrawRequest; +pub use settings::Settings; pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs index e2fb5e66..a14d127f 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -1,10 +1,27 @@ use crate::time::Instant; +use crate::{Point, Size}; use std::path::PathBuf; /// A window-related event. -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum Event { + /// A window was opened. + Opened { + /// The position of the opened window. This is relative to the top-left corner of the desktop + /// the window is on, including virtual desktops. Refers to window's "inner" position, + /// or the client area, in logical pixels. + /// + /// **Note**: Not available in Wayland. + position: Option<Point>, + /// The size of the created window. This is its "inner" size, or the size of the + /// client area, in logical pixels. + size: Size, + }, + + /// A window was closed. + Closed, + /// A window was moved. Moved { /// The new logical x location of the window @@ -27,9 +44,6 @@ pub enum Event { RedrawRequested(Instant), /// 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. @@ -44,7 +58,7 @@ pub enum Event { /// for each file separately. FileHovered(PathBuf), - /// A file has beend dropped into the window. + /// A file has been dropped into the window. /// /// When the user drops multiple files at once, this event will be emitted /// for each file separately. diff --git a/core/src/window/id.rs b/core/src/window/id.rs new file mode 100644 index 00000000..20474c8f --- /dev/null +++ b/core/src/window/id.rs @@ -0,0 +1,21 @@ +use std::hash::Hash; + +use std::sync::atomic::{self, AtomicU64}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// The id of the window. +/// +/// Internally Iced reserves `window::Id::MAIN` for the first window spawned. +pub struct Id(u64); + +static COUNT: AtomicU64 = AtomicU64::new(1); + +impl Id { + /// The reserved window [`Id`] for the first window in an Iced application. + pub const MAIN: Self = Id(0); + + /// Creates a new unique window [`Id`]. + pub fn unique() -> Id { + Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) + } +} diff --git a/winit/src/position.rs b/core/src/window/position.rs index c260c29e..73391e75 100644 --- a/winit/src/position.rs +++ b/core/src/window/position.rs @@ -1,5 +1,7 @@ +use crate::Point; + /// The position of a window in a given screen. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Position { /// The platform-specific default position for a new window. Default, @@ -12,7 +14,7 @@ pub enum Position { /// 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), + Specific(Point), } impl Default for Position { diff --git a/src/window/settings.rs b/core/src/window/settings.rs index 0ee573e5..fbbf86ab 100644 --- a/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -1,21 +1,47 @@ -use crate::window::{Icon, Level, Position}; +//! Configure your windows. +#[cfg(target_os = "windows")] +#[path = "settings/windows.rs"] +mod platform; + +#[cfg(target_os = "macos")] +#[path = "settings/macos.rs"] +mod platform; + +#[cfg(target_os = "linux")] +#[path = "settings/linux.rs"] +mod platform; + +#[cfg(target_arch = "wasm32")] +#[path = "settings/wasm.rs"] +mod platform; -pub use iced_winit::settings::PlatformSpecific; +#[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_arch = "wasm32" +)))] +#[path = "settings/other.rs"] +mod platform; +use crate::window::{Icon, Level, Position}; +use crate::Size; + +pub use platform::PlatformSpecific; /// The window settings of an application. #[derive(Debug, Clone)] pub struct Settings { - /// The initial size of the window. - pub size: (u32, u32), + /// The initial logical dimensions of the window. + pub size: Size, /// The initial position of the window. pub position: Position, /// The minimum size of the window. - pub min_size: Option<(u32, u32)>, + pub min_size: Option<Size>, /// The maximum size of the window. - pub max_size: Option<(u32, u32)>, + pub max_size: Option<Size>, /// Whether the window should be visible or not. pub visible: bool, @@ -37,12 +63,22 @@ pub struct Settings { /// Platform specific settings. pub platform_specific: PlatformSpecific, + + /// Whether the window will close when the user requests it, e.g. when a user presses the + /// close button. + /// + /// This can be useful if you want to have some behavior that executes before the window is + /// actually destroyed. If you disable this, you must manually close the window with the + /// `window::close` command. + /// + /// By default this is enabled. + pub exit_on_close_request: bool, } impl Default for Settings { - fn default() -> Settings { - Settings { - size: (1024, 768), + fn default() -> Self { + Self { + size: Size::new(1024.0, 768.0), position: Position::default(), min_size: None, max_size: None, @@ -52,25 +88,8 @@ impl Default for Settings { transparent: false, level: Level::default(), icon: None, + exit_on_close_request: true, platform_specific: PlatformSpecific::default(), } } } - -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, - visible: settings.visible, - resizable: settings.resizable, - decorations: settings.decorations, - transparent: settings.transparent, - level: settings.level, - icon: settings.icon.map(Icon::into), - platform_specific: settings.platform_specific, - } - } -} diff --git a/winit/src/settings/linux.rs b/core/src/window/settings/linux.rs index 009b9d9e..009b9d9e 100644 --- a/winit/src/settings/linux.rs +++ b/core/src/window/settings/linux.rs diff --git a/winit/src/settings/macos.rs b/core/src/window/settings/macos.rs index f86e63ad..f86e63ad 100644 --- a/winit/src/settings/macos.rs +++ b/core/src/window/settings/macos.rs diff --git a/winit/src/settings/other.rs b/core/src/window/settings/other.rs index b1103f62..b1103f62 100644 --- a/winit/src/settings/other.rs +++ b/core/src/window/settings/other.rs diff --git a/winit/src/settings/wasm.rs b/core/src/window/settings/wasm.rs index 8e0f1bbc..8e0f1bbc 100644 --- a/winit/src/settings/wasm.rs +++ b/core/src/window/settings/wasm.rs diff --git a/winit/src/settings/windows.rs b/core/src/window/settings/windows.rs index 45d753bd..45d753bd 100644 --- a/winit/src/settings/windows.rs +++ b/core/src/window/settings/windows.rs diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 13b08250..cc9ad528 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -26,12 +26,11 @@ mod quad { where Renderer: renderer::Renderer, { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml new file mode 100644 index 00000000..b602f98d --- /dev/null +++ b/examples/custom_shader/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "custom_shader" +version = "0.1.0" +authors = ["Bingus <shankern@protonmail.com>"] +edition = "2021" + +[dependencies] +iced.workspace = true +iced.features = ["debug", "advanced"] + +image.workspace = true +bytemuck.workspace = true + +glam.workspace = true +glam.features = ["bytemuck"] + +rand = "0.8.5" diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs new file mode 100644 index 00000000..3bfa3a43 --- /dev/null +++ b/examples/custom_shader/src/main.rs @@ -0,0 +1,163 @@ +mod scene; + +use scene::Scene; + +use iced::executor; +use iced::time::Instant; +use iced::widget::shader::wgpu; +use iced::widget::{checkbox, column, container, row, shader, slider, text}; +use iced::window; +use iced::{ + Alignment, Application, Color, Command, Element, Length, Renderer, + Subscription, Theme, +}; + +fn main() -> iced::Result { + IcedCubes::run(iced::Settings::default()) +} + +struct IcedCubes { + start: Instant, + scene: Scene, +} + +#[derive(Debug, Clone)] +enum Message { + CubeAmountChanged(u32), + CubeSizeChanged(f32), + Tick(Instant), + ShowDepthBuffer(bool), + LightColorChanged(Color), +} + +impl Application for IcedCubes { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { + ( + Self { + start: Instant::now(), + scene: Scene::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + "Iced Cubes".to_string() + } + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + match message { + Message::CubeAmountChanged(amount) => { + self.scene.change_amount(amount); + } + Message::CubeSizeChanged(size) => { + self.scene.size = size; + } + Message::Tick(time) => { + self.scene.update(time - self.start); + } + Message::ShowDepthBuffer(show) => { + self.scene.show_depth_buffer = show; + } + Message::LightColorChanged(color) => { + self.scene.light_color = color; + } + } + + Command::none() + } + + fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> { + let top_controls = row![ + control( + "Amount", + slider( + 1..=scene::MAX, + self.scene.cubes.len() as u32, + Message::CubeAmountChanged + ) + .width(100) + ), + control( + "Size", + slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged) + .step(0.01) + .width(100), + ), + checkbox( + "Show Depth Buffer", + self.scene.show_depth_buffer, + Message::ShowDepthBuffer + ), + ] + .spacing(40); + + let bottom_controls = row![ + control( + "R", + slider(0.0..=1.0, self.scene.light_color.r, move |r| { + Message::LightColorChanged(Color { + r, + ..self.scene.light_color + }) + }) + .step(0.01) + .width(100) + ), + control( + "G", + slider(0.0..=1.0, self.scene.light_color.g, move |g| { + Message::LightColorChanged(Color { + g, + ..self.scene.light_color + }) + }) + .step(0.01) + .width(100) + ), + control( + "B", + slider(0.0..=1.0, self.scene.light_color.b, move |b| { + Message::LightColorChanged(Color { + b, + ..self.scene.light_color + }) + }) + .step(0.01) + .width(100) + ) + ] + .spacing(40); + + let controls = column![top_controls, bottom_controls,] + .spacing(10) + .padding(20) + .align_items(Alignment::Center); + + let shader = + shader(&self.scene).width(Length::Fill).height(Length::Fill); + + container(column![shader, controls].align_items(Alignment::Center)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + fn subscription(&self) -> Subscription<Self::Message> { + window::frames().map(Message::Tick) + } +} + +fn control<'a>( + label: &'static str, + control: impl Into<Element<'a, Message>>, +) -> Element<'a, Message> { + row![text(label), control.into()].spacing(10).into() +} diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs new file mode 100644 index 00000000..a35efdd9 --- /dev/null +++ b/examples/custom_shader/src/scene.rs @@ -0,0 +1,186 @@ +mod camera; +mod pipeline; + +use camera::Camera; +use pipeline::Pipeline; + +use crate::wgpu; +use pipeline::cube::{self, Cube}; + +use iced::mouse; +use iced::time::Duration; +use iced::widget::shader; +use iced::{Color, Rectangle, Size}; + +use glam::Vec3; +use rand::Rng; +use std::cmp::Ordering; +use std::iter; + +pub const MAX: u32 = 500; + +#[derive(Clone)] +pub struct Scene { + pub size: f32, + pub cubes: Vec<Cube>, + pub camera: Camera, + pub show_depth_buffer: bool, + pub light_color: Color, +} + +impl Scene { + pub fn new() -> Self { + let mut scene = Self { + size: 0.2, + cubes: vec![], + camera: Camera::default(), + show_depth_buffer: false, + light_color: Color::WHITE, + }; + + scene.change_amount(MAX); + + scene + } + + pub fn update(&mut self, time: Duration) { + for cube in self.cubes.iter_mut() { + cube.update(self.size, time.as_secs_f32()); + } + } + + pub fn change_amount(&mut self, amount: u32) { + let curr_cubes = self.cubes.len() as u32; + + match amount.cmp(&curr_cubes) { + Ordering::Greater => { + // spawn + let cubes_2_spawn = (amount - curr_cubes) as usize; + + let mut cubes = 0; + self.cubes.extend(iter::from_fn(|| { + if cubes < cubes_2_spawn { + cubes += 1; + Some(Cube::new(self.size, rnd_origin())) + } else { + None + } + })); + } + Ordering::Less => { + // chop + let cubes_2_cut = curr_cubes - amount; + let new_len = self.cubes.len() - cubes_2_cut as usize; + self.cubes.truncate(new_len); + } + Ordering::Equal => {} + } + } +} + +impl<Message> shader::Program<Message> for Scene { + type State = (); + type Primitive = Primitive; + + fn draw( + &self, + _state: &Self::State, + _cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + Primitive::new( + &self.cubes, + &self.camera, + bounds, + self.show_depth_buffer, + self.light_color, + ) + } +} + +/// A collection of `Cube`s that can be rendered. +#[derive(Debug)] +pub struct Primitive { + cubes: Vec<cube::Raw>, + uniforms: pipeline::Uniforms, + show_depth_buffer: bool, +} + +impl Primitive { + pub fn new( + cubes: &[Cube], + camera: &Camera, + bounds: Rectangle, + show_depth_buffer: bool, + light_color: Color, + ) -> Self { + let uniforms = pipeline::Uniforms::new(camera, bounds, light_color); + + Self { + cubes: cubes + .iter() + .map(cube::Raw::from_cube) + .collect::<Vec<cube::Raw>>(), + uniforms, + show_depth_buffer, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + _bounds: Rectangle, + target_size: Size<u32>, + _scale_factor: f32, + storage: &mut shader::Storage, + ) { + if !storage.has::<Pipeline>() { + storage.store(Pipeline::new(device, queue, format, target_size)); + } + + let pipeline = storage.get_mut::<Pipeline>().unwrap(); + + //upload data to GPU + pipeline.update( + device, + queue, + target_size, + &self.uniforms, + self.cubes.len(), + &self.cubes, + ); + } + + fn render( + &self, + storage: &shader::Storage, + target: &wgpu::TextureView, + _target_size: Size<u32>, + viewport: Rectangle<u32>, + encoder: &mut wgpu::CommandEncoder, + ) { + //at this point our pipeline should always be initialized + let pipeline = storage.get::<Pipeline>().unwrap(); + + //render primitive + pipeline.render( + target, + encoder, + viewport, + self.cubes.len() as u32, + self.show_depth_buffer, + ); + } +} + +fn rnd_origin() -> Vec3 { + Vec3::new( + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..2.0), + ) +} diff --git a/examples/custom_shader/src/scene/camera.rs b/examples/custom_shader/src/scene/camera.rs new file mode 100644 index 00000000..2a49c102 --- /dev/null +++ b/examples/custom_shader/src/scene/camera.rs @@ -0,0 +1,53 @@ +use glam::{mat4, vec3, vec4}; +use iced::Rectangle; + +#[derive(Copy, Clone)] +pub struct Camera { + eye: glam::Vec3, + target: glam::Vec3, + up: glam::Vec3, + fov_y: f32, + near: f32, + far: f32, +} + +impl Default for Camera { + fn default() -> Self { + Self { + eye: vec3(0.0, 2.0, 3.0), + target: glam::Vec3::ZERO, + up: glam::Vec3::Y, + fov_y: 45.0, + near: 0.1, + far: 100.0, + } + } +} + +pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 0.5, 0.0), + vec4(0.0, 0.0, 0.5, 1.0), +); + +impl Camera { + pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 { + //TODO looks distorted without padding; base on surface texture size instead? + let aspect_ratio = bounds.width / (bounds.height + 150.0); + + let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up); + let proj = glam::Mat4::perspective_rh( + self.fov_y, + aspect_ratio, + self.near, + self.far, + ); + + OPENGL_TO_WGPU_MATRIX * proj * view + } + + pub fn position(&self) -> glam::Vec4 { + glam::Vec4::from((self.eye, 0.0)) + } +} diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs new file mode 100644 index 00000000..50b70a98 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -0,0 +1,621 @@ +pub mod cube; + +mod buffer; +mod uniforms; +mod vertex; + +pub use uniforms::Uniforms; + +use buffer::Buffer; +use vertex::Vertex; + +use crate::wgpu; +use crate::wgpu::util::DeviceExt; + +use iced::{Rectangle, Size}; + +const SKY_TEXTURE_SIZE: u32 = 128; + +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, + vertices: wgpu::Buffer, + cubes: Buffer, + uniforms: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + depth_texture_size: Size<u32>, + depth_view: wgpu::TextureView, + depth_pipeline: DepthPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + target_size: Size<u32>, + ) -> Self { + //vertices of one cube + let vertices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("cubes vertex buffer"), + contents: bytemuck::cast_slice(&cube::Raw::vertices()), + usage: wgpu::BufferUsages::VERTEX, + }); + + //cube instance data + let cubes_buffer = Buffer::new( + device, + "cubes instance buffer", + std::mem::size_of::<cube::Raw>() as u64, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + //uniforms for all cubes + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("cubes uniform buffer"), + size: std::mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + //depth buffer + let depth_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: target_size.width, + height: target_size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let normal_map_data = load_normal_map_data(); + + //normal map + let normal_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes normal map texture"), + size: wgpu::Extent3d { + width: 1024, + height: 1024, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + &normal_map_data, + ); + + let normal_view = + normal_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + //skybox texture for reflection/refraction + let skybox_data = load_skybox_data(); + + let skybox_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes skybox texture"), + size: wgpu::Extent3d { + width: SKY_TEXTURE_SIZE, + height: SKY_TEXTURE_SIZE, + depth_or_array_layers: 6, //one for each face of the cube + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + &skybox_data, + ); + + let sky_view = + skybox_texture.create_view(&wgpu::TextureViewDescriptor { + label: Some("cubes skybox texture view"), + dimension: Some(wgpu::TextureViewDimension::Cube), + ..Default::default() + }); + + let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes skybox sampler"), + 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, + ..Default::default() + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes uniform bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let uniform_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes uniform bind group"), + layout: &uniform_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniforms.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&sky_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&sky_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView( + &normal_view, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes pipeline layout"), + bind_group_layouts: &[&uniform_bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shaders/cubes.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc(), cube::Raw::desc()], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + 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::One, + operation: wgpu::BlendOperation::Max, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let depth_pipeline = DepthPipeline::new( + device, + format, + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + + Self { + pipeline, + cubes: cubes_buffer, + uniforms, + uniform_bind_group, + vertices, + depth_texture_size: target_size, + depth_view, + depth_pipeline, + } + } + + fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) { + if self.depth_texture_size.height != size.height + || self.depth_texture_size.width != size.width + { + let text = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + self.depth_view = + text.create_view(&wgpu::TextureViewDescriptor::default()); + self.depth_texture_size = size; + + self.depth_pipeline.update(device, &text); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + uniforms: &Uniforms, + num_cubes: usize, + cubes: &[cube::Raw], + ) { + //recreate depth texture if surface texture size has changed + self.update_depth_texture(device, target_size); + + // update uniforms + queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); + + //resize cubes vertex buffer if cubes amount changed + let new_size = num_cubes * std::mem::size_of::<cube::Raw>(); + self.cubes.resize(device, new_size as u64); + + //always write new cube data since they are constantly rotating + queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes)); + } + + pub fn render( + &self, + target: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + viewport: Rectangle<u32>, + num_cubes: u32, + show_depth: bool, + ) { + { + let mut pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }, + ), + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_scissor_rect( + viewport.x, + viewport.y, + viewport.width, + viewport.height, + ); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_vertex_buffer(0, self.vertices.slice(..)); + pass.set_vertex_buffer(1, self.cubes.raw.slice(..)); + pass.draw(0..36, 0..num_cubes); + } + + if show_depth { + self.depth_pipeline.render(encoder, target, viewport); + } + } +} + +struct DepthPipeline { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + bind_group: wgpu::BindGroup, + sampler: wgpu::Sampler, + depth_view: wgpu::TextureView, +} + +impl DepthPipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + depth_texture: wgpu::TextureView, + ) -> Self { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes.depth_pipeline.sampler"), + ..Default::default() + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes.depth_pipeline.bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &depth_texture, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes.depth_pipeline.layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes.depth_pipeline.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shaders/depth.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes.depth_pipeline.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState::default(), + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + Self { + pipeline, + bind_group_layout, + bind_group, + sampler, + depth_view: depth_texture, + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + depth_texture: &wgpu::Texture, + ) { + self.depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + self.bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &self.depth_view, + ), + }, + ], + }); + } + + pub fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + viewport: Rectangle<u32>, + ) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.depth_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: None, + stencil_ops: None, + }, + ), + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_scissor_rect( + viewport.x, + viewport.y, + viewport.width, + viewport.height, + ); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..6, 0..1); + } +} + +fn load_skybox_data() -> Vec<u8> { + let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg"); + let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg"); + let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg"); + let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg"); + let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg"); + let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg"); + + let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z]; + + data.iter().fold(vec![], |mut acc, bytes| { + let i = image::load_from_memory_with_format( + bytes, + image::ImageFormat::Jpeg, + ) + .unwrap() + .to_rgba8() + .into_raw(); + + acc.extend(i); + acc + }) +} + +fn load_normal_map_data() -> Vec<u8> { + let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png"); + + image::load_from_memory_with_format(bytes, image::ImageFormat::Png) + .unwrap() + .to_rgba8() + .into_raw() +} diff --git a/examples/custom_shader/src/scene/pipeline/buffer.rs b/examples/custom_shader/src/scene/pipeline/buffer.rs new file mode 100644 index 00000000..ef4c41c9 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/buffer.rs @@ -0,0 +1,41 @@ +use crate::wgpu; + +// A custom buffer container for dynamic resizing. +pub struct Buffer { + pub raw: wgpu::Buffer, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, +} + +impl Buffer { + pub fn new( + device: &wgpu::Device, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, + ) -> Self { + Self { + raw: device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }), + label, + size, + usage, + } + } + + pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) { + if new_size > self.size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.label), + size: new_size, + usage: self.usage, + mapped_at_creation: false, + }); + } + } +} diff --git a/examples/custom_shader/src/scene/pipeline/cube.rs b/examples/custom_shader/src/scene/pipeline/cube.rs new file mode 100644 index 00000000..de8bad6c --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/cube.rs @@ -0,0 +1,326 @@ +use crate::scene::pipeline::Vertex; +use crate::wgpu; + +use glam::{vec2, vec3, Vec3}; +use rand::{thread_rng, Rng}; + +/// A single instance of a cube. +#[derive(Debug, Clone)] +pub struct Cube { + pub rotation: glam::Quat, + pub position: Vec3, + pub size: f32, + rotation_dir: f32, + rotation_axis: glam::Vec3, +} + +impl Default for Cube { + fn default() -> Self { + Self { + rotation: glam::Quat::IDENTITY, + position: glam::Vec3::ZERO, + size: 0.1, + rotation_dir: 1.0, + rotation_axis: glam::Vec3::Y, + } + } +} + +impl Cube { + pub fn new(size: f32, origin: Vec3) -> Self { + let rnd = thread_rng().gen_range(0.0..=1.0f32); + + Self { + rotation: glam::Quat::IDENTITY, + position: origin + Vec3::new(0.1, 0.1, 0.1), + size, + rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 }, + rotation_axis: if rnd <= 0.33 { + glam::Vec3::Y + } else if rnd <= 0.66 { + glam::Vec3::X + } else { + glam::Vec3::Z + }, + } + } + + pub fn update(&mut self, size: f32, time: f32) { + self.rotation = glam::Quat::from_axis_angle( + self.rotation_axis, + time / 2.0 * self.rotation_dir, + ); + self.size = size; + } +} + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)] +#[repr(C)] +pub struct Raw { + transformation: glam::Mat4, + normal: glam::Mat3, + _padding: [f32; 3], +} + +impl Raw { + const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ + //cube transformation matrix + 4 => Float32x4, + 5 => Float32x4, + 6 => Float32x4, + 7 => Float32x4, + //normal rotation matrix + 8 => Float32x3, + 9 => Float32x3, + 10 => Float32x3, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &Self::ATTRIBS, + } + } +} + +impl Raw { + pub fn from_cube(cube: &Cube) -> Raw { + Raw { + transformation: glam::Mat4::from_scale_rotation_translation( + glam::vec3(cube.size, cube.size, cube.size), + cube.rotation, + cube.position, + ), + normal: glam::Mat3::from_quat(cube.rotation), + _padding: [0.0; 3], + } + } + + pub fn vertices() -> [Vertex; 36] { + [ + //face 1 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 2 + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 3 + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 4 + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 5 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 6 + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + ] + } +} diff --git a/examples/custom_shader/src/scene/pipeline/uniforms.rs b/examples/custom_shader/src/scene/pipeline/uniforms.rs new file mode 100644 index 00000000..1eac8292 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/uniforms.rs @@ -0,0 +1,23 @@ +use crate::scene::Camera; + +use iced::{Color, Rectangle}; + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Uniforms { + camera_proj: glam::Mat4, + camera_pos: glam::Vec4, + light_color: glam::Vec4, +} + +impl Uniforms { + pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self { + let camera_proj = camera.build_view_proj_matrix(bounds); + + Self { + camera_proj, + camera_pos: camera.position(), + light_color: glam::Vec4::from(light_color.into_linear()), + } + } +} diff --git a/examples/custom_shader/src/scene/pipeline/vertex.rs b/examples/custom_shader/src/scene/pipeline/vertex.rs new file mode 100644 index 00000000..e64cd926 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/vertex.rs @@ -0,0 +1,31 @@ +use crate::wgpu; + +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Vertex { + pub pos: glam::Vec3, + pub normal: glam::Vec3, + pub tangent: glam::Vec3, + pub uv: glam::Vec2, +} + +impl Vertex { + const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ + //position + 0 => Float32x3, + //normal + 1 => Float32x3, + //tangent + 2 => Float32x3, + //uv + 3 => Float32x2, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS, + } + } +} diff --git a/examples/custom_shader/src/shaders/cubes.wgsl b/examples/custom_shader/src/shaders/cubes.wgsl new file mode 100644 index 00000000..cd7f94d8 --- /dev/null +++ b/examples/custom_shader/src/shaders/cubes.wgsl @@ -0,0 +1,123 @@ +struct Uniforms { + projection: mat4x4<f32>, + camera_pos: vec4<f32>, + light_color: vec4<f32>, +} + +const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0); + +@group(0) @binding(0) var<uniform> uniforms: Uniforms; +@group(0) @binding(1) var sky_texture: texture_cube<f32>; +@group(0) @binding(2) var tex_sampler: sampler; +@group(0) @binding(3) var normal_texture: texture_2d<f32>; + +struct Vertex { + @location(0) position: vec3<f32>, + @location(1) normal: vec3<f32>, + @location(2) tangent: vec3<f32>, + @location(3) uv: vec2<f32>, +} + +struct Cube { + @location(4) matrix_0: vec4<f32>, + @location(5) matrix_1: vec4<f32>, + @location(6) matrix_2: vec4<f32>, + @location(7) matrix_3: vec4<f32>, + @location(8) normal_matrix_0: vec3<f32>, + @location(9) normal_matrix_1: vec3<f32>, + @location(10) normal_matrix_2: vec3<f32>, +} + +struct Output { + @builtin(position) clip_pos: vec4<f32>, + @location(0) uv: vec2<f32>, + @location(1) tangent_pos: vec3<f32>, + @location(2) tangent_camera_pos: vec3<f32>, + @location(3) tangent_light_pos: vec3<f32>, +} + +@vertex +fn vs_main(vertex: Vertex, cube: Cube) -> Output { + let cube_matrix = mat4x4<f32>( + cube.matrix_0, + cube.matrix_1, + cube.matrix_2, + cube.matrix_3, + ); + + let normal_matrix = mat3x3<f32>( + cube.normal_matrix_0, + cube.normal_matrix_1, + cube.normal_matrix_2, + ); + + //convert to tangent space to calculate lighting in same coordinate space as normal map sample + let tangent = normalize(normal_matrix * vertex.tangent); + let normal = normalize(normal_matrix * vertex.normal); + let bitangent = cross(tangent, normal); + + //shift everything into tangent space + let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal)); + + let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0); + + var out: Output; + out.clip_pos = uniforms.projection * world_pos; + out.uv = vertex.uv; + out.tangent_pos = tbn * world_pos.xyz; + out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz; + out.tangent_light_pos = tbn * LIGHT_POS; + + return out; +} + +//cube properties +const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6); +const SHINE_DAMPER: f32 = 1.0; +const REFLECTIVITY: f32 = 0.8; +const REFRACTION_INDEX: f32 = 1.31; + +//fog, for the ~* cinematic effect *~ +const FOG_DENSITY: f32 = 0.15; +const FOG_GRADIENT: f32 = 8.0; +const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0); + +@fragment +fn fs_main(in: Output) -> @location(0) vec4<f32> { + let to_camera = in.tangent_camera_pos - in.tangent_pos; + + //normal sample from texture + var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz; + normal = normal * 2.0 - 1.0; + + //diffuse + let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos); + let brightness = max(dot(normal, dir_to_light), 0.0); + let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz; + + //specular + let dir_to_camera = normalize(to_camera); + let light_dir = -dir_to_light; + let reflected_light_dir = reflect(light_dir, normal); + let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0); + let damped_factor = pow(specular_factor, SHINE_DAMPER); + let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY; + + //fog + let distance = length(to_camera); + let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0); + + //reflection + let reflection_dir = reflect(dir_to_camera, normal); + let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir); + let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX); + let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir); + let final_reflect_color = mix(reflection_color, refraction_color, 0.5); + + //mix it all together! + var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w); + color = mix(color, final_reflect_color, 0.8); + color = mix(FOG_COLOR, color, visibility); + + return color; +} diff --git a/examples/custom_shader/src/shaders/depth.wgsl b/examples/custom_shader/src/shaders/depth.wgsl new file mode 100644 index 00000000..a3f7e5ec --- /dev/null +++ b/examples/custom_shader/src/shaders/depth.wgsl @@ -0,0 +1,48 @@ +var<private> 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<private> 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 depth_sampler: sampler; +@group(0) @binding(1) var depth_texture: texture_2d<f32>; + +struct Output { + @builtin(position) position: vec4<f32>, + @location(0) uv: vec2<f32>, +} + +@vertex +fn vs_main(@builtin(vertex_index) v_index: u32) -> Output { + var out: Output; + + out.position = vec4<f32>(positions[v_index], 0.0, 1.0); + out.uv = uvs[v_index]; + + return out; +} + +@fragment +fn fs_main(input: Output) -> @location(0) vec4<f32> { + let depth = textureSample(depth_texture, depth_sampler, input.uv).r; + + if (depth > .9999) { + discard; + } + + let c = 1.0 - depth; + + return vec4<f32>(c, c, c, 1.0); +} diff --git a/examples/custom_shader/textures/ice_cube_normal_map.png b/examples/custom_shader/textures/ice_cube_normal_map.png Binary files differnew file mode 100644 index 00000000..7b4b7228 --- /dev/null +++ b/examples/custom_shader/textures/ice_cube_normal_map.png diff --git a/examples/custom_shader/textures/skybox/neg_x.jpg b/examples/custom_shader/textures/skybox/neg_x.jpg Binary files differnew file mode 100644 index 00000000..00cc783d --- /dev/null +++ b/examples/custom_shader/textures/skybox/neg_x.jpg diff --git a/examples/custom_shader/textures/skybox/neg_y.jpg b/examples/custom_shader/textures/skybox/neg_y.jpg Binary files differnew file mode 100644 index 00000000..548f6445 --- /dev/null +++ b/examples/custom_shader/textures/skybox/neg_y.jpg diff --git a/examples/custom_shader/textures/skybox/neg_z.jpg b/examples/custom_shader/textures/skybox/neg_z.jpg Binary files differnew file mode 100644 index 00000000..5698512e --- /dev/null +++ b/examples/custom_shader/textures/skybox/neg_z.jpg diff --git a/examples/custom_shader/textures/skybox/pos_x.jpg b/examples/custom_shader/textures/skybox/pos_x.jpg Binary files differnew file mode 100644 index 00000000..dddecba7 --- /dev/null +++ b/examples/custom_shader/textures/skybox/pos_x.jpg diff --git a/examples/custom_shader/textures/skybox/pos_y.jpg b/examples/custom_shader/textures/skybox/pos_y.jpg Binary files differnew file mode 100644 index 00000000..361427fd --- /dev/null +++ b/examples/custom_shader/textures/skybox/pos_y.jpg diff --git a/examples/custom_shader/textures/skybox/pos_z.jpg b/examples/custom_shader/textures/skybox/pos_z.jpg Binary files differnew file mode 100644 index 00000000..0085a49e --- /dev/null +++ b/examples/custom_shader/textures/skybox/pos_z.jpg diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 32a14cbe..7ffb4cd0 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -33,12 +33,11 @@ mod circle { where Renderer: renderer::Renderer, { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index a2fcb275..675e9e26 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -73,16 +73,15 @@ impl Application for Example { } fn view(&self) -> Element<Message> { - let downloads = Column::with_children( - self.downloads.iter().map(Download::view).collect(), - ) - .push( - button("Add another download") - .on_press(Message::Add) - .padding(10), - ) - .spacing(20) - .align_items(Alignment::End); + let downloads = + Column::with_children(self.downloads.iter().map(Download::view)) + .push( + button("Add another download") + .on_press(Message::Add) + .padding(10), + ) + .spacing(20) + .align_items(Alignment::End); container(downloads) .width(Length::Fill) diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml new file mode 100644 index 00000000..dc885728 --- /dev/null +++ b/examples/editor/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "editor" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["highlighter", "tokio", "debug"] + +tokio.workspace = true +tokio.features = ["fs"] + +rfd = "0.13" diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf Binary files differnew file mode 100644 index 00000000..393c6922 --- /dev/null +++ b/examples/editor/fonts/icons.ttf diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs new file mode 100644 index 00000000..bf2aaaa3 --- /dev/null +++ b/examples/editor/src/main.rs @@ -0,0 +1,312 @@ +use iced::executor; +use iced::highlighter::{self, Highlighter}; +use iced::keyboard; +use iced::theme::{self, Theme}; +use iced::widget::{ + button, column, container, horizontal_space, pick_list, row, text, + text_editor, tooltip, +}; +use iced::{ + Alignment, Application, Command, Element, Font, Length, Settings, + Subscription, +}; + +use std::ffi; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +pub fn main() -> iced::Result { + Editor::run(Settings { + fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], + default_font: Font::MONOSPACE, + ..Settings::default() + }) +} + +struct Editor { + file: Option<PathBuf>, + content: text_editor::Content, + theme: highlighter::Theme, + is_loading: bool, + is_dirty: bool, +} + +#[derive(Debug, Clone)] +enum Message { + ActionPerformed(text_editor::Action), + ThemeSelected(highlighter::Theme), + NewFile, + OpenFile, + FileOpened(Result<(PathBuf, Arc<String>), Error>), + SaveFile, + FileSaved(Result<PathBuf, Error>), +} + +impl Application for Editor { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Command::perform(load_file(default_file()), Message::FileOpened), + ) + } + + fn title(&self) -> String { + String::from("Editor - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::ActionPerformed(action) => { + self.is_dirty = self.is_dirty || action.is_edit(); + + self.content.perform(action); + + Command::none() + } + Message::ThemeSelected(theme) => { + self.theme = theme; + + Command::none() + } + Message::NewFile => { + if !self.is_loading { + self.file = None; + self.content = text_editor::Content::new(); + } + + Command::none() + } + Message::OpenFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + Command::perform(open_file(), Message::FileOpened) + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + if let Ok((path, contents)) = result { + self.file = Some(path); + self.content = text_editor::Content::with_text(&contents); + } + + Command::none() + } + Message::SaveFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + Command::perform( + save_file(self.file.clone(), self.content.text()), + Message::FileSaved, + ) + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + if let Ok(path) = result { + self.file = Some(path); + self.is_dirty = false; + } + + Command::none() + } + } + } + + fn subscription(&self) -> Subscription<Message> { + keyboard::on_key_press(|key, modifiers| match key.as_ref() { + keyboard::Key::Character("s") if modifiers.command() => { + Some(Message::SaveFile) + } + _ => None, + }) + } + + fn view(&self) -> Element<Message> { + let controls = row![ + action(new_icon(), "New file", Some(Message::NewFile)), + action( + open_icon(), + "Open file", + (!self.is_loading).then_some(Message::OpenFile) + ), + action( + save_icon(), + "Save file", + self.is_dirty.then_some(Message::SaveFile) + ), + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .text_size(14) + .padding([5, 10]) + ] + .spacing(10) + .align_items(Alignment::Center); + + let status = row![ + text(if let Some(path) = &self.file { + let path = path.display().to_string(); + + if path.len() > 60 { + format!("...{}", &path[path.len() - 40..]) + } else { + path + } + } else { + String::from("New file") + }), + horizontal_space(Length::Fill), + text({ + let (line, column) = self.content.cursor_position(); + + format!("{}:{}", line + 1, column + 1) + }) + ] + .spacing(10); + + column![ + controls, + text_editor(&self.content) + .on_action(Message::ActionPerformed) + .highlight::<Highlighter>( + highlighter::Settings { + theme: self.theme, + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), + }, + |highlight, _theme| highlight.to_format() + ), + status, + ] + .spacing(10) + .padding(10) + .into() + } + + fn theme(&self) -> Theme { + if self.theme.is_dark() { + Theme::Dark + } else { + Theme::Light + } + } +} + +#[derive(Debug, Clone)] +pub enum Error { + DialogClosed, + IoError(io::ErrorKind), +} + +fn default_file() -> PathBuf { + PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))) +} + +async fn open_file() -> Result<(PathBuf, Arc<String>), Error> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a text file...") + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + load_file(picked_file.path().to_owned()).await +} + +async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc<String>), Error> { + let contents = tokio::fs::read_to_string(&path) + .await + .map(Arc::new) + .map_err(|error| Error::IoError(error.kind()))?; + + Ok((path, contents)) +} + +async fn save_file( + path: Option<PathBuf>, + contents: String, +) -> Result<PathBuf, Error> { + let path = if let Some(path) = path { + path + } else { + rfd::AsyncFileDialog::new() + .save_file() + .await + .as_ref() + .map(rfd::FileHandle::path) + .map(Path::to_owned) + .ok_or(Error::DialogClosed)? + }; + + tokio::fs::write(&path, contents) + .await + .map_err(|error| Error::IoError(error.kind()))?; + + Ok(path) +} + +fn action<'a, Message: Clone + 'a>( + content: impl Into<Element<'a, Message>>, + label: &'a str, + on_press: Option<Message>, +) -> Element<'a, Message> { + let action = button(container(content).width(30).center_x()); + + if let Some(on_press) = on_press { + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(theme::Container::Box) + .into() + } else { + action.style(theme::Button::Secondary).into() + } +} + +fn new_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e800}') +} + +fn save_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e801}') +} + +fn open_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0f115}') +} + +fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { + const ICON_FONT: Font = Font::with_name("editor-icons"); + + text(codepoint).font(ICON_FONT).into() +} diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 32d0da2c..fc51ac4a 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -10,7 +10,10 @@ use iced::{ pub fn main() -> iced::Result { Events::run(Settings { - exit_on_close_request: false, + window: window::Settings { + exit_on_close_request: false, + ..window::Settings::default() + }, ..Settings::default() }) } @@ -54,8 +57,9 @@ impl Application for Events { Command::none() } Message::EventOccurred(event) => { - if let Event::Window(window::Event::CloseRequested) = event { - window::close() + if let Event::Window(id, window::Event::CloseRequested) = event + { + window::close(id) } else { Command::none() } @@ -65,7 +69,7 @@ impl Application for Events { Command::none() } - Message::Exit => window::close(), + Message::Exit => window::close(window::Id::MAIN), } } @@ -78,8 +82,7 @@ impl Application for Events { self.last .iter() .map(|event| text(format!("{event:?}")).size(40)) - .map(Element::from) - .collect(), + .map(Element::from), ); let toggle = checkbox( diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 6152f627..ec618dc1 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -34,7 +34,7 @@ impl Application for Exit { fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::Confirm => window::close(), + Message::Confirm => window::close(window::Id::MAIN), Message::Exit => { self.show_confirm = true; diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 9b291de8..7596844c 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -9,7 +9,7 @@ publish = false iced.workspace = true iced.features = ["debug", "canvas", "tokio"] -itertools = "0.11" +itertools = "0.12" rustc-hash.workspace = true tokio = { workspace = true, features = ["sync"] } tracing-subscriber = "0.3" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 96840143..56f7afd5 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -146,7 +146,8 @@ impl Application for GameOfLife { .view() .map(move |message| Message::Grid(message, version)), controls, - ]; + ] + .height(Length::Fill); container(content) .width(Length::Fill) @@ -178,7 +179,6 @@ fn view_controls<'a>( slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), text(format!("x{speed}")).size(16), ] - .width(Length::Fill) .align_items(Alignment::Center) .spacing(10); diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 8ab3b493..5cf9963d 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -16,12 +16,11 @@ mod rainbow { } impl<Message> Widget<Message, Renderer> for Rainbow { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( @@ -30,9 +29,9 @@ mod rainbow { _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.width(Length::Fill).resolve(Size::ZERO); + let width = limits.max().width; - layout::Node::new(Size::new(size.width, size.width)) + layout::Node::new(Size::new(width, width)) } fn draw( diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 4714c397..89a595c1 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -81,32 +81,25 @@ impl Program for Controls { ); Row::new() - .width(Length::Fill) .height(Length::Fill) .align_items(Alignment::End) .push( - Column::new() - .width(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .padding(10) - .spacing(10) - .push( - Text::new("Background color") - .style(Color::WHITE), - ) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push( - text_input("Placeholder", text) - .on_input(Message::TextChanged), - ), - ), + Column::new().align_items(Alignment::End).push( + Column::new() + .padding(10) + .spacing(10) + .push(Text::new("Background color").style(Color::WHITE)) + .push(sliders) + .push( + Text::new(format!("{background_color:?}")) + .size(14) + .style(Color::WHITE), + ) + .push( + text_input("Placeholder", text) + .on_input(Message::TextChanged), + ), + ), ) .into() } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c26d52fe..ed61459f 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -6,19 +6,26 @@ use scene::Scene; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; +use iced_winit::core::window; use iced_winit::core::{Color, Font, Pixels, Size}; +use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; use iced_winit::style::Theme; -use iced_winit::{conversion, futures, winit, Clipboard}; +use iced_winit::winit; +use iced_winit::Clipboard; use winit::{ - event::{Event, ModifiersState, WindowEvent}, + event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::ModifiersState, }; +use std::sync::Arc; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] @@ -44,7 +51,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { tracing_subscriber::fmt::init(); // Initialize winit - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new()?; #[cfg(target_arch = "wasm32")] let window = winit::window::WindowBuilder::new() @@ -54,6 +61,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { #[cfg(not(target_arch = "wasm32"))] let window = winit::window::Window::new(&event_loop)?; + let window = Arc::new(window); + let physical_size = window.inner_size(); let mut viewport = Viewport::with_physical_size( Size::new(physical_size.width, physical_size.height), @@ -76,7 +85,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { backends: backend, ..Default::default() }); - let surface = unsafe { instance.create_surface(&window) }?; + let surface = instance.create_surface(window.clone())?; let (format, (device, queue)) = futures::futures::executor::block_on(async { @@ -110,9 +119,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { .request_device( &wgpu::DeviceDescriptor { label: None, - features: adapter_features + required_features: adapter_features & wgpu::Features::default(), - limits: needed_limits, + required_limits: needed_limits, }, None, ) @@ -131,6 +140,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); @@ -156,66 +166,15 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { ); // Run event loop - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, window_target| { // You should change this if you want to render continuosly - *control_flow = ControlFlow::Wait; + window_target.set_control_flow(ControlFlow::Wait); match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = Some(position); - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - WindowEvent::Resized(_) => { - resized = true; - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - _ => {} - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - &event, - window.scale_factor(), - modifiers, - ) { - 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(), - cursor_position - .map(|p| { - conversion::cursor_position( - p, - viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable), - &mut renderer, - &Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - &mut clipboard, - &mut debug, - ); - - // and request a redraw - window.request_redraw(); - } - } - Event::RedrawRequested(_) => { + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { if resized { let size = window.inner_size(); @@ -234,6 +193,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); @@ -271,6 +231,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { &queue, &mut encoder, None, + frame.texture.format(), &view, primitive, &viewport, @@ -303,7 +264,60 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { }, } } + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = Some(position); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + modifiers = new_modifiers.state(); + } + WindowEvent::Resized(_) => { + resized = true; + } + WindowEvent::CloseRequested => { + window_target.exit(); + } + _ => {} + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, + event, + window.scale_factor(), + modifiers, + ) { + state.queue_event(event); + } + } _ => {} } - }) + + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + cursor_position + .map(|p| { + conversion::cursor_position(p, viewport.scale_factor()) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable), + &mut renderer, + &Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, + }, + &mut clipboard, + &mut debug, + ); + + // and request a redraw + window.request_redraw(); + } + })?; + + Ok(()) } diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 01808f40..e29558bf 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -36,10 +36,12 @@ impl Scene { a: a as f64, } }), - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }) } diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml new file mode 100644 index 00000000..855f98d0 --- /dev/null +++ b/examples/layout/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "layout" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs new file mode 100644 index 00000000..6cf0e570 --- /dev/null +++ b/examples/layout/src/main.rs @@ -0,0 +1,371 @@ +use iced::executor; +use iced::keyboard; +use iced::mouse; +use iced::theme; +use iced::widget::{ + button, canvas, checkbox, column, container, horizontal_space, pick_list, + row, scrollable, text, vertical_rule, +}; +use iced::{ + color, Alignment, Application, Color, Command, Element, Font, Length, + Point, Rectangle, Renderer, Settings, Subscription, Theme, +}; + +pub fn main() -> iced::Result { + Layout::run(Settings::default()) +} + +#[derive(Debug)] +struct Layout { + example: Example, + explain: bool, + theme: Theme, +} + +#[derive(Debug, Clone)] +enum Message { + Next, + Previous, + ExplainToggled(bool), + ThemeSelected(Theme), +} + +impl Application for Layout { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + Self { + example: Example::default(), + explain: false, + theme: Theme::Light, + }, + Command::none(), + ) + } + + fn title(&self) -> String { + format!("{} - Layout - Iced", self.example.title) + } + + fn update(&mut self, message: Self::Message) -> Command<Message> { + match message { + Message::Next => { + self.example = self.example.next(); + } + Message::Previous => { + self.example = self.example.previous(); + } + Message::ExplainToggled(explain) => { + self.explain = explain; + } + Message::ThemeSelected(theme) => { + self.theme = theme; + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + use keyboard::key; + + keyboard::on_key_release(|key, _modifiers| match key { + keyboard::Key::Named(key::Named::ArrowLeft) => { + Some(Message::Previous) + } + keyboard::Key::Named(key::Named::ArrowRight) => Some(Message::Next), + _ => None, + }) + } + + fn view(&self) -> Element<Message> { + let header = row![ + text(self.example.title).size(20).font(Font::MONOSPACE), + horizontal_space(Length::Fill), + checkbox("Explain", self.explain, Message::ExplainToggled), + pick_list( + Theme::ALL, + Some(self.theme.clone()), + Message::ThemeSelected + ), + ] + .spacing(20) + .align_items(Alignment::Center); + + let example = container(if self.explain { + self.example.view().explain(color!(0x0000ff)) + } else { + self.example.view() + }) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default() + .with_border(palette.background.strong.color, 4.0) + }) + .padding(4) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); + + let controls = row([ + (!self.example.is_first()).then_some( + button("← Previous") + .padding([5, 10]) + .on_press(Message::Previous) + .into(), + ), + Some(horizontal_space(Length::Fill).into()), + (!self.example.is_last()).then_some( + button("Next →") + .padding([5, 10]) + .on_press(Message::Next) + .into(), + ), + ] + .into_iter() + .flatten()); + + column![header, example, controls] + .spacing(10) + .padding(20) + .into() + } + + fn theme(&self) -> Theme { + self.theme.clone() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Example { + title: &'static str, + view: fn() -> Element<'static, Message>, +} + +impl Example { + const LIST: &'static [Self] = &[ + Self { + title: "Centered", + view: centered, + }, + Self { + title: "Column", + view: column_, + }, + Self { + title: "Row", + view: row_, + }, + Self { + title: "Space", + view: space, + }, + Self { + title: "Application", + view: application, + }, + Self { + title: "Nested Quotes", + view: nested_quotes, + }, + ]; + + fn is_first(self) -> bool { + Self::LIST.first() == Some(&self) + } + + fn is_last(self) -> bool { + Self::LIST.last() == Some(&self) + } + + fn previous(self) -> Self { + let Some(index) = + Self::LIST.iter().position(|&example| example == self) + else { + return self; + }; + + Self::LIST + .get(index.saturating_sub(1)) + .copied() + .unwrap_or(self) + } + + fn next(self) -> Self { + let Some(index) = + Self::LIST.iter().position(|&example| example == self) + else { + return self; + }; + + Self::LIST.get(index + 1).copied().unwrap_or(self) + } + + fn view(&self) -> Element<Message> { + (self.view)() + } +} + +impl Default for Example { + fn default() -> Self { + Self::LIST[0] + } +} + +fn centered<'a>() -> Element<'a, Message> { + container(text("I am centered!").size(50)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() +} + +fn column_<'a>() -> Element<'a, Message> { + column![ + "A column can be used to", + "lay out widgets vertically.", + square(50), + square(50), + square(50), + "The amount of space between", + "elements can be configured!", + ] + .spacing(40) + .into() +} + +fn row_<'a>() -> Element<'a, Message> { + row![ + "A row works like a column...", + square(50), + square(50), + square(50), + "but lays out widgets horizontally!", + ] + .spacing(40) + .into() +} + +fn space<'a>() -> Element<'a, Message> { + row!["Left!", horizontal_space(Length::Fill), "Right!"].into() +} + +fn application<'a>() -> Element<'a, Message> { + let header = container( + row![ + square(40), + horizontal_space(Length::Fill), + "Header!", + horizontal_space(Length::Fill), + square(40), + ] + .padding(10) + .align_items(Alignment::Center), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default() + .with_border(palette.background.strong.color, 1) + }); + + let sidebar = container( + column!["Sidebar!", square(50), square(50)] + .spacing(40) + .padding(10) + .width(200) + .align_items(Alignment::Center), + ) + .style(theme::Container::Box) + .height(Length::Fill) + .center_y(); + + let content = container( + scrollable( + column![ + "Content!", + square(400), + square(200), + square(400), + "The end" + ] + .spacing(40) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .height(Length::Fill), + ) + .padding(10); + + column![header, row![sidebar, content]].into() +} + +fn nested_quotes<'a>() -> Element<'a, Message> { + (1..5) + .fold(column![text("Original text")].padding(10), |quotes, i| { + column![ + container( + row![vertical_rule(2), quotes].height(Length::Shrink) + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default().with_background( + if palette.is_dark { + Color { + a: 0.01, + ..Color::WHITE + } + } else { + Color { + a: 0.08, + ..Color::BLACK + } + }, + ) + }), + text(format!("Reply {i}")) + ] + .spacing(10) + .padding(10) + }) + .into() +} + +fn square<'a>(size: impl Into<Length> + Copy) -> Element<'a, Message> { + struct Square; + + impl canvas::Program<Message> for Square { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec<canvas::Geometry> { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + + let palette = theme.extended_palette(); + + frame.fill_rectangle( + Point::ORIGIN, + bounds.size(), + palette.background.strong.color, + ); + + vec![frame.into_geometry()] + } + } + + canvas(Square).width(size).height(size).into() +} diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 9bf17c56..04df0744 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -46,7 +46,7 @@ enum Color { } impl Color { - const ALL: &[Color] = &[ + const ALL: &'static [Color] = &[ Color::Black, Color::Red, Color::Orange, @@ -178,35 +178,23 @@ impl Sandbox for App { } }); - column( - items - .into_iter() - .map(|item| { - let button = button("Delete") - .on_press(Message::DeleteItem(item.clone())) - .style(theme::Button::Destructive); - - row![ - text(&item.name) - .style(theme::Text::Color(item.color.into())), - horizontal_space(Length::Fill), - pick_list( - Color::ALL, - Some(item.color), - move |color| { - Message::ItemColorChanged( - item.clone(), - color, - ) - } - ), - button - ] - .spacing(20) - .into() - }) - .collect(), - ) + column(items.into_iter().map(|item| { + let button = button("Delete") + .on_press(Message::DeleteItem(item.clone())) + .style(theme::Button::Destructive); + + row![ + text(&item.name) + .style(theme::Text::Color(item.color.into())), + horizontal_space(Length::Fill), + pick_list(Color::ALL, Some(item.color), move |color| { + Message::ItemColorChanged(item.clone(), color) + }), + button + ] + .spacing(20) + .into() + })) .spacing(10) }); diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index bf01c3b4..2e119979 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -244,12 +244,11 @@ where tree::State::new(State::default()) } - fn width(&self) -> Length { - Length::Fixed(self.size) - } - - fn height(&self) -> Length { - Length::Fixed(self.size) + fn size(&self) -> Size<Length> { + Size { + width: Length::Fixed(self.size), + height: Length::Fixed(self.size), + } } fn layout( @@ -258,10 +257,7 @@ where _renderer: &iced::Renderer<Theme>, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.size).height(self.size); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic(limits, self.size, self.size) } fn on_event( @@ -275,11 +271,9 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - const FRAME_RATE: u64 = 60; - let state = tree.state.downcast_mut::<State>(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, @@ -287,9 +281,7 @@ where ); state.cache.clear(); - shell.request_redraw(RedrawRequest::At( - now + Duration::from_millis(1000 / FRAME_RATE), - )); + shell.request_redraw(RedrawRequest::NextFrame); } event::Status::Ignored diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index c5bb4791..497e0834 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -165,12 +165,11 @@ where tree::State::new(State::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -179,10 +178,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( @@ -196,16 +192,12 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - const FRAME_RATE: u64 = 60; - let state = tree.state.downcast_mut::<State>(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); - shell.request_redraw(RedrawRequest::At( - now + Duration::from_millis(1000 / FRAME_RATE), - )); + shell.request_redraw(RedrawRequest::NextFrame); } event::Status::Ignored diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index a78e9590..93a4605e 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -96,15 +96,14 @@ impl Application for LoadingSpinners { container( column.push( - row(vec![ - text("Cycle duration:").into(), + row![ + text("Cycle duration:"), slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { Message::CycleDurationChanged(x / 100.0) }) - .width(200.0) - .into(), - text(format!("{:.2}s", self.cycle_duration)).into(), - ]) + .width(200.0), + text(format!("{:.2}s", self.cycle_duration)), + ] .align_items(iced::Alignment::Center) .spacing(20.0), ), diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index b0e2c81b..963c839e 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -1,6 +1,7 @@ use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::keyboard::key; use iced::theme; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, @@ -85,8 +86,9 @@ impl Application for App { } Message::Event(event) => match event { Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), modifiers, + .. }) => { if modifiers.shift() { widget::focus_previous() @@ -95,7 +97,7 @@ impl Application for App { } } Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Escape, + key: keyboard::Key::Named(key::Named::Escape), .. }) => { self.hide_modal(); @@ -205,7 +207,8 @@ enum Plan { } impl Plan { - pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise]; + pub const ALL: &'static [Self] = + &[Self::Basic, Self::Pro, Self::Enterprise]; } impl fmt::Display for Plan { @@ -230,6 +233,7 @@ mod modal { use iced::mouse; use iced::{ BorderRadius, Color, Element, Event, Length, Point, Rectangle, Size, + Vector, }; /// A widget that centers a modal element over some base element @@ -279,12 +283,8 @@ mod modal { tree.diff_children(&[&self.base, &self.modal]); } - fn width(&self) -> Length { - self.base.as_widget().width() - } - - fn height(&self) -> Length { - self.base.as_widget().height() + fn size(&self) -> Size<Length> { + self.base.as_widget().size() } fn layout( @@ -412,22 +412,20 @@ mod modal { renderer: &Renderer, _bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, self.size) .width(Length::Fill) .height(Length::Fill); - let mut child = self + let child = self .content .as_widget() - .layout(self.tree, renderer, &limits); - - child.align(Alignment::Center, Alignment::Center, limits.max()); - - let mut node = layout::Node::with_children(self.size, vec![child]); - node.move_to(position); + .layout(self.tree, renderer, &limits) + .align(Alignment::Center, Alignment::Center, limits.max()); - node + layout::Node::with_children(self.size, vec![child]) + .move_to(position) } fn on_event( diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml new file mode 100644 index 00000000..2e222dfb --- /dev/null +++ b/examples/multi_window/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "multi_window" +version = "0.1.0" +authors = ["Bingus <shankern@protonmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "multi-window"] } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs new file mode 100644 index 00000000..5a5e70c1 --- /dev/null +++ b/examples/multi_window/src/main.rs @@ -0,0 +1,215 @@ +use iced::event; +use iced::executor; +use iced::multi_window::{self, Application}; +use iced::widget::{button, column, container, scrollable, text, text_input}; +use iced::window; +use iced::{ + Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, + Vector, +}; + +use std::collections::HashMap; + +fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + windows: HashMap<window::Id, Window>, + next_window_pos: window::Position, +} + +#[derive(Debug)] +struct Window { + title: String, + scale_input: String, + current_scale: f64, + theme: Theme, + input_id: iced::widget::text_input::Id, +} + +#[derive(Debug, Clone)] +enum Message { + ScaleInputChanged(window::Id, String), + ScaleChanged(window::Id, String), + TitleChanged(window::Id, String), + CloseWindow(window::Id), + WindowOpened(window::Id, Option<Point>), + WindowClosed(window::Id), + NewWindow, +} + +impl multi_window::Application for Example { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Example { + windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), + next_window_pos: window::Position::Default, + }, + Command::none(), + ) + } + + fn title(&self, window: window::Id) -> String { + self.windows + .get(&window) + .map(|window| window.title.clone()) + .unwrap_or("Example".to_string()) + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::ScaleInputChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + window.scale_input = scale; + + Command::none() + } + Message::ScaleChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + + window.current_scale = scale + .parse::<f64>() + .unwrap_or(window.current_scale) + .clamp(0.5, 5.0); + + Command::none() + } + Message::TitleChanged(id, title) => { + let window = + self.windows.get_mut(&id).expect("Window not found."); + + window.title = title; + + Command::none() + } + Message::CloseWindow(id) => window::close(id), + Message::WindowClosed(id) => { + self.windows.remove(&id); + Command::none() + } + Message::WindowOpened(id, position) => { + if let Some(position) = position { + self.next_window_pos = window::Position::Specific( + position + Vector::new(20.0, 20.0), + ); + } + + if let Some(window) = self.windows.get(&id) { + text_input::focus(window.input_id.clone()) + } else { + Command::none() + } + } + Message::NewWindow => { + let count = self.windows.len() + 1; + + let (id, spawn_window) = window::spawn(window::Settings { + position: self.next_window_pos, + exit_on_close_request: count % 2 == 0, + ..Default::default() + }); + + self.windows.insert(id, Window::new(count)); + + spawn_window + } + } + } + + fn view(&self, window: window::Id) -> Element<Message> { + let content = self.windows.get(&window).unwrap().view(window); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + fn theme(&self, window: window::Id) -> Self::Theme { + self.windows.get(&window).unwrap().theme.clone() + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.windows + .get(&window) + .map(|window| window.current_scale) + .unwrap_or(1.0) + } + + fn subscription(&self) -> Subscription<Self::Message> { + event::listen_with(|event, _| { + if let iced::Event::Window(id, window_event) = event { + match window_event { + window::Event::CloseRequested => { + Some(Message::CloseWindow(id)) + } + window::Event::Opened { position, .. } => { + Some(Message::WindowOpened(id, position)) + } + window::Event::Closed => Some(Message::WindowClosed(id)), + _ => None, + } + } else { + None + } + }) + } +} + +impl Window { + fn new(count: usize) -> Self { + Self { + title: format!("Window_{}", count), + scale_input: "1.0".to_string(), + current_scale: 1.0, + theme: if count % 2 == 0 { + Theme::Light + } else { + Theme::Dark + }, + input_id: text_input::Id::unique(), + } + } + + fn view(&self, id: window::Id) -> Element<Message> { + let scale_input = column![ + text("Window scale factor:"), + text_input("Window Scale", &self.scale_input) + .on_input(move |msg| { Message::ScaleInputChanged(id, msg) }) + .on_submit(Message::ScaleChanged( + id, + self.scale_input.to_string() + )) + ]; + + let title_input = column![ + text("Window title:"), + text_input("Window Title", &self.title) + .on_input(move |msg| { Message::TitleChanged(id, msg) }) + .id(self.input_id.clone()) + ]; + + let new_window_button = + button(text("New Window")).on_press(Message::NewWindow); + + let content = scrollable( + column![scale_input, title_input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_items(Alignment::Center), + ); + + container(content).width(200).center_x().into() + } +} diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index aa3149bb..d5e5bcbe 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -220,23 +220,26 @@ const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( 0x47 as f32 / 255.0, ); -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { - use keyboard::KeyCode; +fn handle_hotkey(key: keyboard::Key) -> Option<Message> { + use keyboard::key::{self, Key}; use pane_grid::{Axis, Direction}; - let direction = match key_code { - KeyCode::Up => Some(Direction::Up), - KeyCode::Down => Some(Direction::Down), - KeyCode::Left => Some(Direction::Left), - KeyCode::Right => Some(Direction::Right), - _ => None, - }; + match key.as_ref() { + Key::Character("v") => Some(Message::SplitFocused(Axis::Vertical)), + Key::Character("h") => Some(Message::SplitFocused(Axis::Horizontal)), + Key::Character("w") => Some(Message::CloseFocused), + Key::Named(key) => { + let direction = match key { + key::Named::ArrowUp => Some(Direction::Up), + key::Named::ArrowDown => Some(Direction::Down), + key::Named::ArrowLeft => Some(Direction::Left), + key::Named::ArrowRight => Some(Direction::Right), + _ => None, + }; - match key_code { - KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), - KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), - KeyCode::W => Some(Message::CloseFocused), - _ => direction.map(Message::FocusAdjacent), + direction.map(Message::FocusAdjacent) + } + _ => None, } } @@ -297,7 +300,6 @@ fn view_content<'a>( text(format!("{}x{}", size.width, size.height)).size(24), controls, ] - .width(Length::Fill) .spacing(10) .align_items(Alignment::Center); diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 21200621..e4d96dc8 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{column, container, pick_list, scrollable, vertical_space}; +use iced::widget::{column, pick_list, scrollable, vertical_space}; use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { @@ -52,12 +52,7 @@ impl Sandbox for Example { .align_items(Alignment::Center) .spacing(10); - container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + scrollable(content).into() } } diff --git a/examples/progress_bar/README.md b/examples/progress_bar/README.md index 1268ac6b..a87829c6 100644 --- a/examples/progress_bar/README.md +++ b/examples/progress_bar/README.md @@ -5,7 +5,7 @@ A simple progress bar that can be filled by using a slider. The __[`main`]__ file contains all the code of the example. <div align="center"> - <img src="https://iced.rs/examples/pokedex.gif"> + <img src="https://iced.rs/examples/progress_bar.gif"> </div> You can run it with `cargo run`: diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index ab0a2ae3..6955551e 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,11 +1,13 @@ use iced::alignment; -use iced::keyboard::KeyCode; -use iced::theme::{Button, Container}; +use iced::executor; +use iced::keyboard; +use iced::theme; use iced::widget::{button, column, container, image, row, text, text_input}; +use iced::window; use iced::window::screenshot::{self, Screenshot}; use iced::{ - event, executor, keyboard, Alignment, Application, Command, ContentFit, - Element, Event, Length, Rectangle, Renderer, Subscription, Theme, + Alignment, Application, Command, ContentFit, Element, Length, Rectangle, + Renderer, Subscription, Theme, }; use ::image as img; @@ -70,7 +72,10 @@ impl Application for Example { fn update(&mut self, message: Self::Message) -> Command<Self::Message> { match message { Message::Screenshot => { - return iced::window::screenshot(Message::ScreenshotData); + return iced::window::screenshot( + window::Id::MAIN, + Message::ScreenshotData, + ); } Message::ScreenshotData(screenshot) => { self.screenshot = Some(screenshot); @@ -144,7 +149,7 @@ impl Application for Example { let image = container(image) .padding(10) - .style(Container::Box) + .style(theme::Container::Box) .width(Length::FillPortion(2)) .height(Length::Fill) .center_x() @@ -199,9 +204,10 @@ impl Application for Example { self.screenshot.is_some().then(|| Message::Png), ) } else { - button(centered_text("Saving...")).style(Button::Secondary) + button(centered_text("Saving...")) + .style(theme::Button::Secondary) } - .style(Button::Secondary) + .style(theme::Button::Secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) ] @@ -210,7 +216,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(Button::Destructive) + .style(theme::Button::Destructive) .padding([10, 20, 10, 20]) .width(Length::Fill), ] @@ -253,16 +259,10 @@ impl Application for Example { } fn subscription(&self) -> Subscription<Self::Message> { - event::listen_with(|event, status| { - if let event::Status::Captured = status { - return None; - } + use keyboard::key; - if let Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::F5, - .. - }) = event - { + keyboard::on_key_press(|key, _modifiers| { + if let keyboard::Key::Named(key::Named::F5) = key { Some(Message::Screenshot) } else { None @@ -298,10 +298,7 @@ fn numeric_input( ) -> Element<'_, Option<u32>> { text_input( placeholder, - &value - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(String::new), + &value.as_ref().map(ToString::to_string).unwrap_or_default(), ) .on_input(move |text| { if text.is_empty() { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index d82ea841..4b57a5a4 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -147,63 +147,54 @@ impl Application for ScrollableDemo { text("Scroller width:"), scroller_width_slider, ] - .spacing(10) - .width(Length::Fill); + .spacing(10); - let scroll_orientation_controls = column(vec![ - text("Scrollbar direction:").into(), + let scroll_orientation_controls = column![ + text("Scrollbar direction:"), radio( "Vertical", Direction::Vertical, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), + ), radio( "Horizontal", Direction::Horizontal, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), + ), radio( "Both!", Direction::Multi, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), - ]) - .spacing(10) - .width(Length::Fill); + ), + ] + .spacing(10); - let scroll_alignment_controls = column(vec![ - text("Scrollable alignment:").into(), + let scroll_alignment_controls = column![ + text("Scrollable alignment:"), radio( "Start", scrollable::Alignment::Start, Some(self.alignment), Message::AlignmentChanged, - ) - .into(), + ), radio( "End", scrollable::Alignment::End, Some(self.alignment), Message::AlignmentChanged, ) - .into(), - ]) - .spacing(10) - .width(Length::Fill); + ] + .spacing(10); let scroll_controls = row![ scroll_slider_controls, scroll_orientation_controls, scroll_alignment_controls ] - .spacing(20) - .width(Length::Fill); + .spacing(20); let scroll_to_end_button = || { button("Scroll to end") @@ -229,11 +220,11 @@ impl Application for ScrollableDemo { text("End!"), scroll_to_beginning_button(), ] - .width(Length::Fill) .align_items(Alignment::Center) .padding([40, 0, 40, 0]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction(scrollable::Direction::Vertical( Properties::new() @@ -259,6 +250,7 @@ impl Application for ScrollableDemo { .padding([0, 40, 0, 40]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction(scrollable::Direction::Horizontal( Properties::new() @@ -301,6 +293,7 @@ impl Application for ScrollableDemo { .padding([0, 40, 0, 40]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction({ let properties = Properties::new() @@ -341,20 +334,11 @@ impl Application for ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .width(Length::Fill) - .height(Length::Fill) .align_items(Alignment::Center) .spacing(10) .into(); - Element::from( - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(40) - .center_x() - .center_y(), - ) + container(content).padding(20).center_x().center_y().into() } fn theme(&self) -> Self::Theme { diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index ef935c33..01a114bb 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -79,12 +79,10 @@ impl Application for SierpinskiEmulator { row![ text(format!("Iteration: {:?}", self.graph.iteration)), slider(0..=10000, self.graph.iteration, Message::IterationSet) - .width(Length::Fill) ] .padding(10) .spacing(20), ] - .width(Length::Fill) .align_items(iced::Alignment::Center) .into() } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 8295dded..82421a86 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -114,14 +114,14 @@ impl State { pub fn new() -> State { let now = Instant::now(); - let (width, height) = window::Settings::default().size; + let size = window::Settings::default().size; State { space_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(), start: now, now, - stars: Self::generate_stars(width, height), + stars: Self::generate_stars(size.width, size.height), } } @@ -130,7 +130,7 @@ impl State { self.system_cache.clear(); } - fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> { use rand::Rng; let mut rng = rand::thread_rng(); @@ -139,12 +139,8 @@ impl State { .map(|_| { ( Point::new( - rng.gen_range( - (-(width as f32) / 2.0)..(width as f32 / 2.0), - ), - rng.gen_range( - (-(height as f32) / 2.0)..(height as f32 / 2.0), - ), + rng.gen_range((-width / 2.0)..(width / 2.0)), + rng.gen_range((-height / 2.0)..(height / 2.0)), ), rng.gen_range(0.5..1.0), ) diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 0b0f0607..8a0674c1 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -86,12 +86,16 @@ impl Application for Stopwatch { }; fn handle_hotkey( - key_code: keyboard::KeyCode, + key: keyboard::Key, _modifiers: keyboard::Modifiers, ) -> Option<Message> { - match key_code { - keyboard::KeyCode::Space => Some(Message::Toggle), - keyboard::KeyCode::R => Some(Message::Reset), + use keyboard::key; + + match key.as_ref() { + keyboard::Key::Named(key::Named::Space) => { + Some(Message::Toggle) + } + keyboard::Key::Character("r") => Some(Message::Reset), _ => None, } } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 51538ec2..10f3c79d 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -53,13 +53,16 @@ impl Sandbox for Styling { self.theme = match theme { ThemeType::Light => Theme::Light, ThemeType::Dark => Theme::Dark, - ThemeType::Custom => Theme::custom(theme::Palette { - background: Color::from_rgb(1.0, 0.9, 1.0), - text: Color::BLACK, - primary: Color::from_rgb(0.5, 0.5, 0.0), - success: Color::from_rgb(0.0, 1.0, 0.0), - danger: Color::from_rgb(1.0, 0.0, 0.0), - }), + ThemeType::Custom => Theme::custom( + String::from("Custom"), + theme::Palette { + background: Color::from_rgb(1.0, 0.9, 1.0), + text: Color::BLACK, + primary: Color::from_rgb(0.5, 0.5, 0.0), + success: Color::from_rgb(0.0, 1.0, 0.0), + danger: Color::from_rgb(1.0, 0.0, 0.0), + }, + ), } } Message::InputChanged(value) => self.input_value = value, @@ -104,10 +107,11 @@ impl Sandbox for Styling { let progress_bar = progress_bar(0.0..=100.0, self.slider_value); - let scrollable = scrollable( - column!["Scroll me!", vertical_space(800), "You did it!"] - .width(Length::Fill), - ) + let scrollable = scrollable(column![ + "Scroll me!", + vertical_space(800), + "You did it!" + ]) .width(Length::Fill) .height(100); diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 4dc92416..3bf4960f 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -63,7 +63,6 @@ impl Sandbox for Tiger { container(apply_color_filter).width(Length::Fill).center_x() ] .spacing(20) - .width(Length::Fill) .height(Length::Fill), ) .width(Length::Fill) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 20c3dd42..2e837fa3 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -1,6 +1,7 @@ use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::keyboard::key; use iced::widget::{ self, button, column, container, pick_list, row, slider, text, text_input, }; @@ -93,11 +94,12 @@ impl Application for App { Command::none() } Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), modifiers, + .. })) if modifiers.shift() => widget::focus_previous(), Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), .. })) => widget::focus_next(), Message::Event(_) => Command::none(), @@ -106,9 +108,7 @@ impl Application for App { fn view<'a>(&'a self) -> Element<'a, Message> { let subtitle = |title, content: Element<'a, Message>| { - column![text(title).size(14), content] - .width(Length::Fill) - .spacing(5) + column![text(title).size(14), content].spacing(5) }; let mut add_toast = button("Add Toast"); @@ -153,14 +153,11 @@ impl Application for App { Message::Timeout ) .step(1.0) - .width(Length::Fill) ] .spacing(5) .into() ), - column![add_toast] - .width(Length::Fill) - .align_items(Alignment::End) + column![add_toast].align_items(Alignment::End) ] .spacing(10) .max_width(200), @@ -210,7 +207,7 @@ mod toast { } impl Status { - pub const ALL: &[Self] = + pub const ALL: &'static [Self] = &[Self::Primary, Self::Secondary, Self::Success, Self::Danger]; } @@ -318,12 +315,8 @@ mod toast { } impl<'a, Message> Widget<Message, Renderer> for Manager<'a, Message> { - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( @@ -511,15 +504,16 @@ mod toast { renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { - let limits = layout::Limits::new(Size::ZERO, bounds) - .width(Length::Fill) - .height(Length::Fill); + let limits = layout::Limits::new(Size::ZERO, bounds); layout::flex::resolve( layout::flex::Axis::Vertical, renderer, &limits, + Length::Fill, + Length::Fill, 10.into(), 10.0, Alignment::End, @@ -538,7 +532,9 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Window(window::Event::RedrawRequested(now)) = &event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = + &event + { let mut next_redraw: Option<window::RedrawRequest> = None; self.instants.iter_mut().enumerate().for_each( diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 1ad3aba7..3d79f087 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -8,7 +8,7 @@ use iced::widget::{ }; use iced::window; use iced::{Application, Element}; -use iced::{Color, Command, Length, Settings, Subscription}; +use iced::{Color, Command, Length, Settings, Size, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ pub fn main() -> iced::Result { Todos::run(Settings { window: window::Settings { - size: (500, 800), + size: Size::new(500.0, 800.0), ..window::Settings::default() }, ..Settings::default() @@ -54,7 +54,7 @@ enum Message { FilterChanged(Filter), TaskMessage(usize, TaskMessage), TabPressed { shift: bool }, - ChangeWindowMode(window::Mode), + ToggleFullscreen(window::Mode), } impl Application for Todos { @@ -165,8 +165,8 @@ impl Application for Todos { widget::focus_next() } } - Message::ChangeWindowMode(mode) => { - window::change_mode(mode) + Message::ToggleFullscreen(mode) => { + window::change_mode(window::Id::MAIN, mode) } _ => Command::none(), }; @@ -254,28 +254,28 @@ impl Application for Todos { .spacing(20) .max_width(800); - scrollable( - container(content) - .width(Length::Fill) - .padding(40) - .center_x(), - ) - .into() + scrollable(container(content).padding(40).center_x()).into() } } } fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_press(|key_code, modifiers| { - match (key_code, modifiers) { - (keyboard::KeyCode::Tab, _) => Some(Message::TabPressed { + use keyboard::key; + + keyboard::on_key_press(|key, modifiers| { + let keyboard::Key::Named(key) = key else { + return None; + }; + + match (key, modifiers) { + (key::Named::Tab, _) => Some(Message::TabPressed { shift: modifiers.shift(), }), - (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => { - Some(Message::ChangeWindowMode(window::Mode::Fullscreen)) + (key::Named::ArrowUp, keyboard::Modifiers::SHIFT) => { + Some(Message::ToggleFullscreen(window::Mode::Fullscreen)) } - (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => { - Some(Message::ChangeWindowMode(window::Mode::Windowed)) + (key::Named::ArrowDown, keyboard::Modifiers::SHIFT) => { + Some(Message::ToggleFullscreen(window::Mode::Windowed)) } _ => None, } @@ -472,7 +472,6 @@ fn empty_message(message: &str) -> Element<'_, Message> { .horizontal_alignment(alignment::Horizontal::Center) .style(Color::from([0.7, 0.7, 0.7])), ) - .width(Length::Fill) .height(200) .center_y() .into() diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index d46e40d1..8633bc0a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,4 +1,4 @@ -use iced::alignment; +use iced::alignment::{self, Alignment}; use iced::theme; use iced::widget::{ checkbox, column, container, horizontal_space, image, radio, row, @@ -126,7 +126,10 @@ impl Steps { Step::Toggler { can_continue: false, }, - Step::Image { width: 300 }, + Step::Image { + width: 300, + filter_method: image::FilterMethod::Linear, + }, Step::Scrollable, Step::TextInput { value: String::new(), @@ -195,6 +198,7 @@ enum Step { }, Image { width: u16, + filter_method: image::FilterMethod, }, Scrollable, TextInput { @@ -215,6 +219,7 @@ pub enum StepMessage { TextColorChanged(Color), LanguageSelected(Language), ImageWidthChanged(u16), + ImageUseNearestToggled(bool), InputChanged(String), ToggleSecureInput(bool), ToggleTextInputIcon(bool), @@ -265,6 +270,15 @@ impl<'a> Step { *width = new_width; } } + StepMessage::ImageUseNearestToggled(use_nearest) => { + if let Step::Image { filter_method, .. } = self { + *filter_method = if use_nearest { + image::FilterMethod::Nearest + } else { + image::FilterMethod::Linear + }; + } + } StepMessage::InputChanged(new_value) => { if let Step::TextInput { value, .. } = self { *value = new_value; @@ -330,7 +344,10 @@ impl<'a> Step { Step::Toggler { can_continue } => Self::toggler(*can_continue), Step::Slider { value } => Self::slider(*value), Step::Text { size, color } => Self::text(*size, *color), - Step::Image { width } => Self::image(*width), + Step::Image { + width, + filter_method, + } => Self::image(*width, *filter_method), Step::RowsAndColumns { layout, spacing } => { Self::rows_and_columns(*layout, *spacing) } @@ -492,7 +509,6 @@ impl<'a> Step { ) }) .map(Element::from) - .collect() ) .spacing(10) ] @@ -525,16 +541,25 @@ impl<'a> Step { ) } - fn image(width: u16) -> Column<'a, StepMessage> { + fn image( + width: u16, + filter_method: image::FilterMethod, + ) -> Column<'a, StepMessage> { Self::container("Image") .push("An image that tries to keep its aspect ratio.") - .push(ferris(width)) + .push(ferris(width, filter_method)) .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push( text(format!("Width: {width} px")) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) + .push(checkbox( + "Use nearest interpolation", + filter_method == image::FilterMethod::Nearest, + StepMessage::ImageUseNearestToggled, + )) + .align_items(Alignment::Center) } fn scrollable() -> Column<'a, StepMessage> { @@ -555,7 +580,7 @@ impl<'a> Step { .horizontal_alignment(alignment::Horizontal::Center), ) .push(vertical_space(4096)) - .push(ferris(300)) + .push(ferris(300, image::FilterMethod::Linear)) .push( text("You made it!") .width(Length::Fill) @@ -646,7 +671,10 @@ impl<'a> Step { } } -fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { +fn ferris<'a>( + width: u16, + filter_method: image::FilterMethod, +) -> Container<'a, StepMessage> { container( // This should go away once we unify resource loading on native // platforms @@ -655,6 +683,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { } else { image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } + .filter_method(filter_method) .width(width), ) .width(Length::Fill) @@ -662,11 +691,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { } fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button( - text(label).horizontal_alignment(alignment::Horizontal::Center), - ) - .padding(12) - .width(100) + iced::widget::button(text(label)).padding([12, 24]) } fn color_slider<'a>( diff --git a/examples/vectorial_text/Cargo.toml b/examples/vectorial_text/Cargo.toml new file mode 100644 index 00000000..76c1af7c --- /dev/null +++ b/examples/vectorial_text/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vectorial_text" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "debug"] } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs new file mode 100644 index 00000000..d366b907 --- /dev/null +++ b/examples/vectorial_text/src/main.rs @@ -0,0 +1,175 @@ +use iced::alignment::{self, Alignment}; +use iced::mouse; +use iced::widget::{ + canvas, checkbox, column, horizontal_space, row, slider, text, +}; +use iced::{ + Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme, + Vector, +}; + +pub fn main() -> iced::Result { + VectorialText::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct VectorialText { + state: State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SizeChanged(f32), + AngleChanged(f32), + ScaleChanged(f32), + ToggleJapanese(bool), +} + +impl Sandbox for VectorialText { + type Message = Message; + + fn new() -> Self { + Self { + state: State::new(), + } + } + + fn title(&self) -> String { + String::from("Vectorial Text - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::SizeChanged(size) => { + self.state.size = size; + } + Message::AngleChanged(angle) => { + self.state.angle = angle; + } + Message::ScaleChanged(scale) => { + self.state.scale = scale; + } + Message::ToggleJapanese(use_japanese) => { + self.state.use_japanese = use_japanese; + } + } + + self.state.cache.clear(); + } + + fn view(&self) -> Element<Message> { + let slider_with_label = |label, range, value, message: fn(f32) -> _| { + column![ + row![ + text(label), + horizontal_space(Length::Fill), + text(format!("{:.2}", value)) + ], + slider(range, value, message).step(0.01) + ] + .spacing(2) + }; + + column![ + canvas(&self.state).width(Length::Fill).height(Length::Fill), + column![ + checkbox( + "Use Japanese", + self.state.use_japanese, + Message::ToggleJapanese + ), + row![ + slider_with_label( + "Size", + 2.0..=80.0, + self.state.size, + Message::SizeChanged, + ), + slider_with_label( + "Angle", + 0.0..=360.0, + self.state.angle, + Message::AngleChanged, + ), + slider_with_label( + "Scale", + 1.0..=20.0, + self.state.scale, + Message::ScaleChanged, + ), + ] + .spacing(20), + ] + .align_items(Alignment::Center) + .spacing(10) + ] + .spacing(10) + .padding(20) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +struct State { + size: f32, + angle: f32, + scale: f32, + use_japanese: bool, + cache: canvas::Cache, +} + +impl State { + pub fn new() -> Self { + Self { + size: 40.0, + angle: 0.0, + scale: 1.0, + use_japanese: false, + cache: canvas::Cache::new(), + } + } +} + +impl<Message> canvas::Program<Message> for State { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec<canvas::Geometry> { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { + let palette = theme.palette(); + let center = bounds.center(); + + frame.translate(Vector::new(center.x, center.y)); + frame.scale(self.scale); + frame.rotate(self.angle * std::f32::consts::PI / 180.0); + + frame.fill_text(canvas::Text { + position: Point::new(0.0, 0.0), + color: palette.text, + size: self.size.into(), + content: String::from(if self.use_japanese { + "ベクトルテキスト🎉" + } else { + "Vectorial Text! 🎉" + }), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + ..canvas::Text::default() + }); + }); + + vec![geometry] + } +} diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 697badb4..fdf1e0f9 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -167,7 +167,7 @@ impl Application for Example { Event::Mouse(mouse::Event::CursorMoved { position }) => { Some(Message::MouseMoved(position)) } - Event::Window(window::Event::Resized { .. }) => { + Event::Window(_, window::Event::Resized { .. }) => { Some(Message::WindowResized) } _ => None, diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index 2756e8e0..8f1b876a 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -13,7 +13,7 @@ once_cell.workspace = true warp = "0.3" [dependencies.async-tungstenite] -version = "0.23" +version = "0.24" features = ["tokio-rustls-webpki-roots"] [dependencies.tokio] diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 920189f5..38a6db1e 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -3,7 +3,7 @@ mod echo; use iced::alignment::{self, Alignment}; use iced::executor; use iced::widget::{ - button, column, container, row, scrollable, text, text_input, Column, + button, column, container, row, scrollable, text, text_input, }; use iced::{ Application, Color, Command, Element, Length, Settings, Subscription, Theme, @@ -108,15 +108,9 @@ impl Application for WebSocket { .into() } else { scrollable( - Column::with_children( - self.messages - .iter() - .cloned() - .map(text) - .map(Element::from) - .collect(), + column( + self.messages.iter().cloned().map(text).map(Element::from), ) - .width(Length::Fill) .spacing(10), ) .id(MESSAGE_LOG.clone()) @@ -131,7 +125,7 @@ impl Application for WebSocket { let mut button = button( text("Send") - .height(Length::Fill) + .height(40) .vertical_alignment(alignment::Vertical::Center), ) .padding([0, 20]); @@ -149,7 +143,6 @@ impl Application for WebSocket { }; column![message_log, new_message_input] - .width(Length::Fill) .height(Length::Fill) .padding(20) .spacing(10) diff --git a/futures/src/event.rs b/futures/src/event.rs index 214d2d40..97224506 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -35,7 +35,7 @@ where subscription::filter_map( (EventsWith, f), move |event, status| match event { - Event::Window(window::Event::RedrawRequested(_)) => None, + Event::Window(_, window::Event::RedrawRequested(_)) => None, _ => f(event, status), }, ) diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index af68e1f2..8e7da38f 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -1,6 +1,6 @@ //! Listen to keyboard events. use crate::core; -use crate::core::keyboard::{Event, KeyCode, Modifiers}; +use crate::core::keyboard::{Event, Key, Modifiers}; use crate::subscription::{self, Subscription}; use crate::MaybeSend; @@ -10,7 +10,7 @@ use crate::MaybeSend; /// If the function returns `None`, the key press will be simply /// ignored. pub fn on_key_press<Message>( - f: fn(KeyCode, Modifiers) -> Option<Message>, + f: fn(Key, Modifiers) -> Option<Message>, ) -> Subscription<Message> where Message: MaybeSend + 'static, @@ -22,11 +22,10 @@ where match (event, status) { ( core::Event::Keyboard(Event::KeyPressed { - key_code, - modifiers, + key, modifiers, .. }), core::event::Status::Ignored, - ) => f(key_code, modifiers), + ) => f(key, modifiers), _ => None, } }) @@ -38,7 +37,7 @@ where /// If the function returns `None`, the key release will be simply /// ignored. pub fn on_key_release<Message>( - f: fn(KeyCode, Modifiers) -> Option<Message>, + f: fn(Key, Modifiers) -> Option<Message>, ) -> Subscription<Message> where Message: MaybeSend + 'static, @@ -50,11 +49,12 @@ where match (event, status) { ( core::Event::Keyboard(Event::KeyReleased { - key_code, + key, modifiers, + .. }), core::event::Status::Ignored, - ) => f(key_code, modifiers), + ) => f(key, modifiers), _ => None, } }) diff --git a/futures/src/lib.rs b/futures/src/lib.rs index d54ba18a..b0acb76f 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -15,7 +15,7 @@ pub use futures; pub use iced_core as core; -mod maybe_send; +mod maybe; mod runtime; pub mod backend; @@ -25,7 +25,7 @@ pub mod keyboard; pub mod subscription; pub use executor::Executor; -pub use maybe_send::MaybeSend; +pub use maybe::{MaybeSend, MaybeSync}; pub use platform::*; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/futures/src/maybe.rs b/futures/src/maybe.rs new file mode 100644 index 00000000..c6a507c1 --- /dev/null +++ b/futures/src/maybe.rs @@ -0,0 +1,35 @@ +#[cfg(not(target_arch = "wasm32"))] +mod platform { + /// An extension trait that enforces `Send` only on native platforms. + /// + /// Useful for writing cross-platform async code! + pub trait MaybeSend: Send {} + + impl<T> MaybeSend for T where T: Send {} + + /// An extension trait that enforces `Sync` only on native platforms. + /// + /// Useful for writing cross-platform async code! + pub trait MaybeSync: Sync {} + + impl<T> MaybeSync for T where T: Sync {} +} + +#[cfg(target_arch = "wasm32")] +mod platform { + /// An extension trait that enforces `Send` only on native platforms. + /// + /// Useful for writing cross-platform async code! + pub trait MaybeSend {} + + impl<T> MaybeSend for T {} + + /// An extension trait that enforces `Sync` only on native platforms. + /// + /// Useful for writing cross-platform async code! + pub trait MaybeSync {} + + impl<T> MaybeSync for T {} +} + +pub use platform::{MaybeSend, MaybeSync}; diff --git a/futures/src/maybe_send.rs b/futures/src/maybe_send.rs deleted file mode 100644 index a6670f0e..00000000 --- a/futures/src/maybe_send.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -mod platform { - /// An extension trait that enforces `Send` only on native platforms. - /// - /// Useful to write cross-platform async code! - pub trait MaybeSend: Send {} - - impl<T> MaybeSend for T where T: Send {} -} - -#[cfg(target_arch = "wasm32")] -mod platform { - /// An extension trait that enforces `Send` only on native platforms. - /// - /// Useful to write cross-platform async code! - pub trait MaybeSend {} - - impl<T> MaybeSend for T {} -} - -pub use platform::MaybeSend; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 16111b36..cac7b7e1 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,7 +1,7 @@ //! Run commands and keep track of subscriptions. use crate::core::event::{self, Event}; use crate::subscription; -use crate::{BoxFuture, Executor, MaybeSend}; +use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; use futures::{channel::mpsc, Sink}; use std::marker::PhantomData; @@ -69,6 +69,29 @@ where self.executor.spawn(future); } + /// Runs a [`Stream`] in the [`Runtime`] until completion. + /// + /// The resulting `Message`s will be forwarded to the `Sender` of the + /// [`Runtime`]. + /// + /// [`Stream`]: BoxStream + pub fn run(&mut self, stream: BoxStream<Message>) { + use futures::{FutureExt, StreamExt}; + + let sender = self.sender.clone(); + let future = + stream.map(Ok).forward(sender).map(|result| match result { + Ok(()) => (), + Err(error) => { + log::warn!( + "Stream could not run until completion: {error}" + ); + } + }); + + self.executor.spawn(future); + } + /// Tracks a [`Subscription`] in the [`Runtime`]. /// /// It will spawn new streams or close old ones as necessary! See diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index ff698649..4f323f9e 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -16,25 +16,25 @@ all-features = true [features] geometry = ["lyon_path"] -opengl = [] image = ["dep:image", "kamadak-exif"] web-colors = [] [dependencies] iced_core.workspace = true +iced_futures.workspace = true bitflags.workspace = true bytemuck.workspace = true +cosmic-text.workspace = true glam.workspace = true half.workspace = true log.workspace = true +once_cell.workspace = true raw-window-handle.workspace = true -thiserror.workspace = true -cosmic-text.workspace = true rustc-hash.workspace = true - -lyon_path.workspace = true -lyon_path.optional = true +thiserror.workspace = true +unicode-segmentation.workspace = true +xxhash-rust.workspace = true image.workspace = true image.optional = true @@ -42,8 +42,5 @@ image.optional = true kamadak-exif.workspace = true kamadak-exif.optional = true -twox-hash.workspace = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -twox-hash.workspace = true -twox-hash.features = ["std"] +lyon_path.workspace = true +lyon_path.optional = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index c2ac82ba..10eb337f 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -2,7 +2,6 @@ use crate::core::image; use crate::core::svg; use crate::core::Size; -use crate::text; use std::borrow::Cow; @@ -18,9 +17,6 @@ pub trait Backend { pub trait Text { /// Loads a font from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - - /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`]. - fn font_system(&self) -> &text::FontSystem; } /// A graphics backend that supports image rendering. diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0222a80f..0188f4d8 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -2,9 +2,10 @@ //! surfaces. use crate::{Error, Viewport}; -use iced_core::Color; +use crate::core::Color; +use crate::futures::{MaybeSend, MaybeSync}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; /// A graphics compositor that can draw to windows. @@ -19,17 +20,20 @@ pub trait Compositor: Sized { type Surface; /// Creates a new [`Compositor`]. - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new<W: Window + Clone>( settings: Self::Settings, - compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error>; + compatible_window: W, + ) -> Result<Self, Error>; + + /// Creates a [`Self::Renderer`] for the [`Compositor`]. + fn create_renderer(&self) -> Self::Renderer; /// Crates a new [`Surface`] for the given window. /// /// [`Surface`]: Self::Surface - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface<W: Window + Clone>( &mut self, - window: &W, + window: W, width: u32, height: u32, ) -> Self::Surface; @@ -74,6 +78,20 @@ pub trait Compositor: Sized { ) -> Vec<u8>; } +/// A window that can be used in a [`Compositor`]. +/// +/// This is just a convenient super trait of the `raw-window-handle` +/// traits. +pub trait Window: + HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +impl<T> Window for T where + T: HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + /// Result of an unsuccessful call to [`Compositor::present`]. #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 3276c2d4..59e9f5b4 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -66,6 +66,18 @@ impl<T: Damage> Damage for Primitive<T> { bounds.expand(1.5) } + Self::Editor { + editor, position, .. + } => { + let bounds = Rectangle::new(*position, editor.bounds); + + bounds.expand(1.5) + } + Self::RawText(raw) => { + // TODO: Add `size` field to `raw` to compute more accurate + // damage bounds (?) + raw.clip_bounds.expand(1.5) + } Self::Quad { bounds, .. } | Self::Image { bounds, .. } | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 0bf7ec97..d314e85e 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,6 +1,8 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point}; +use crate::core::{Color, Font, Pixels, Point, Size, Vector}; +use crate::geometry::Path; +use crate::text; /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] @@ -32,6 +34,137 @@ pub struct Text { pub shaping: Shaping, } +impl Text { + /// Computes the [`Path`]s of the [`Text`] and draws them using + /// the given closure. + pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::BufferLine::new( + &self.content, + cosmic_text::AttrsList::new(text::to_attributes(self.font)), + text::to_shaping(self.shaping), + ); + + let layout = buffer.layout( + font_system.raw(), + self.size.0, + f32::MAX, + cosmic_text::Wrap::None, + ); + + let translation_x = match self.horizontal_alignment { + alignment::Horizontal::Left => self.position.x, + alignment::Horizontal::Center | alignment::Horizontal::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); + } + + if self.horizontal_alignment == alignment::Horizontal::Center { + self.position.x - line_width / 2.0 + } else { + self.position.x - line_width + } + } + }; + + let translation_y = { + let line_height = self.line_height.to_absolute(self.size); + + match self.vertical_alignment { + alignment::Vertical::Top => self.position.y, + alignment::Vertical::Center => { + self.position.y - line_height.0 / 2.0 + } + alignment::Vertical::Bottom => self.position.y - line_height.0, + } + }; + + let mut swash_cache = cosmic_text::SwashCache::new(); + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + self.size.0; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_glyph.cache_key, + ) { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo(control_a, control_b, to) => { + path.bezier_curve_to( + Point::new(control_a.x, -control_a.y) + + offset, + Point::new(control_b.x, -control_b.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + f(glyph, self.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = self.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + f( + Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ); + } + } + } + } +} + impl Default for Text { fn default() -> Text { Text { diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index a0729058..76de56bf 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links @@ -50,3 +50,4 @@ pub use transformation::Transformation; pub use viewport::Viewport; pub use iced_core as core; +pub use iced_futures as futures; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 8a97e6e7..20affaaf 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -4,6 +4,7 @@ use crate::core::image; use crate::core::svg; use crate::core::text; use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; +use crate::text::editor; use crate::text::paragraph; use std::sync::Arc; @@ -13,24 +14,26 @@ use std::sync::Arc; pub enum Primitive<T> { /// A text primitive Text { - /// The contents of the text + /// The contents of the text. content: String, - /// The bounds of the text + /// The bounds of the text. bounds: Rectangle, - /// The color of the text + /// The color of the text. color: Color, - /// The size of the text in logical pixels + /// The size of the text in logical pixels. size: Pixels, - /// The line height of the text + /// The line height of the text. line_height: text::LineHeight, - /// The font of the text + /// The font of the text. font: Font, - /// The horizontal alignment of the text + /// The horizontal alignment of the text. horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text + /// The vertical alignment of the text. vertical_alignment: alignment::Vertical, /// The shaping strategy of the text. shaping: text::Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, }, /// A paragraph primitive Paragraph { @@ -40,7 +43,22 @@ pub enum Primitive<T> { position: Point, /// The color of the paragraph. color: Color, + /// The clip bounds of the paragraph. + clip_bounds: Rectangle, }, + /// An editor primitive + Editor { + /// The [`editor::Weak`] reference. + editor: editor::Weak, + /// The position of the editor. + position: Point, + /// The color of the editor. + color: Color, + /// The clip bounds of the editor. + clip_bounds: Rectangle, + }, + /// A raw `cosmic-text` primitive + RawText(crate::text::Raw), /// A quad primitive Quad { /// The bounds of the quad @@ -58,6 +76,8 @@ pub enum Primitive<T> { Image { /// The handle of the image handle: image::Handle, + /// The filter method of the image + filter_method: image::FilterMethod, /// The bounds of the image bounds: Rectangle, }, diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index a9d7895e..1b0f5c5b 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -141,6 +141,7 @@ where { type Font = Font; type Paragraph = text::Paragraph; + type Editor = text::Editor; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -158,51 +159,33 @@ where self.backend.load_font(bytes); } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph { - text::Paragraph::with_text(text, self.backend.font_system()) - } - - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - let font_system = self.backend.font_system(); - - if paragraph.version() != font_system.version() { - // The font system has changed, paragraph fonts may be outdated - *paragraph = self.create_paragraph(text); - } else { - match core::text::compare(paragraph, text) { - core::text::Difference::None => {} - core::text::Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - core::text::Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - } - - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ) { - paragraph.resize(new_bounds, self.backend.font_system()); - } - fn fill_paragraph( &mut self, paragraph: &Self::Paragraph, position: Point, color: Color, + clip_bounds: Rectangle, ) { self.primitives.push(Primitive::Paragraph { paragraph: paragraph.downgrade(), position, color, + clip_bounds, + }); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.primitives.push(Primitive::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, }); } @@ -211,6 +194,7 @@ where text: Text<'_, Self::Font>, position: Point, color: Color, + clip_bounds: Rectangle, ) { self.primitives.push(Primitive::Text { content: text.content.to_string(), @@ -222,6 +206,7 @@ where horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + clip_bounds, }); } } @@ -236,8 +221,17 @@ where self.backend().dimensions(handle) } - fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.primitives.push(Primitive::Image { handle, bounds }); + fn draw( + &mut self, + handle: image::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + ) { + self.primitives.push(Primitive::Image { + handle, + filter_method, + bounds, + }); } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index bc06aa3c..7c4b5e31 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -1,69 +1,96 @@ +//! Draw text. pub mod cache; +pub mod editor; pub mod paragraph; pub use cache::Cache; +pub use editor::Editor; pub use paragraph::Paragraph; pub use cosmic_text; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::Size; +use crate::core::{Color, Point, Rectangle, Size}; +use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{self, Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak}; +/// Returns the global [`FontSystem`]. +pub fn font_system() -> &'static RwLock<FontSystem> { + static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new(); + + FONT_SYSTEM.get_or_init(|| { + RwLock::new(FontSystem { + raw: cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), + )), + ]), + version: Version::default(), + }) + }) +} + +/// A set of system fonts. #[allow(missing_debug_implementations)] pub struct FontSystem { - raw: RwLock<cosmic_text::FontSystem>, + raw: cosmic_text::FontSystem, version: Version, } impl FontSystem { - pub fn new() -> Self { - FontSystem { - raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts( - [cosmic_text::fontdb::Source::Binary(Arc::new( - include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), - ))] - .into_iter(), - )), - version: Version::default(), - } - } - - pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem { - self.raw.get_mut().expect("Lock font system") - } - - pub fn write( - &self, - ) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) { - (self.raw.write().expect("Write font system"), self.version) + /// Returns the raw [`cosmic_text::FontSystem`]. + pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.raw } + /// Loads a font from its bytes. pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - let _ = self.get_mut().db_mut().load_font_source( + let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); self.version = Version(self.version.0 + 1); } + /// Returns the current [`Version`] of the [`FontSystem`]. + /// + /// Loading a font will increase the version of a [`FontSystem`]. pub fn version(&self) -> Version { self.version } } +/// A version number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); -impl Default for FontSystem { - fn default() -> Self { - Self::new() +/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { + /// A weak reference to a [`cosmic_text::Buffer`]. + pub buffer: Weak<cosmic_text::Buffer>, + /// The position of the text. + pub position: Point, + /// The color of the text. + pub color: Color, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { + fn eq(&self, _other: &Self) -> bool { + // TODO: There is no proper way to compare raw buffers + // For now, no two instances of `Raw` text will be equal. + // This should be fine, but could trigger unnecessary redraws + // in the future. + false } } +/// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() @@ -71,9 +98,15 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { (run.line_w.max(width), total_lines + 1) }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + let (max_width, max_height) = buffer.size(); + + Size::new( + width.min(max_width), + (total_lines as f32 * buffer.metrics().line_height).min(max_height), + ) } +/// Returns the attributes of the given [`Font`]. pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { cosmic_text::Attrs::new() .family(to_family(font.family)) @@ -129,9 +162,17 @@ fn to_style(style: font::Style) -> cosmic_text::Style { } } +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { match shaping { Shaping::Basic => cosmic_text::Shaping::Basic, Shaping::Advanced => cosmic_text::Shaping::Advanced, } } + +/// Converts some [`Color`] to a [`cosmic_text::Color`]. +pub fn to_color(color: Color) -> cosmic_text::Color { + let [r, g, b, a] = color.into_rgba8(); + + cosmic_text::Color::rgba(r, g, b, a) +} diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 577c4687..7fb33567 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -1,3 +1,4 @@ +//! Cache text. use crate::core::{Font, Size}; use crate::text; @@ -5,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; +/// A store of recently used sections of text. #[allow(missing_debug_implementations)] #[derive(Default)] pub struct Cache { @@ -14,21 +16,20 @@ pub struct Cache { hasher: HashBuilder, } -#[cfg(not(target_arch = "wasm32"))] -type HashBuilder = twox_hash::RandomXxHashBuilder64; - -#[cfg(target_arch = "wasm32")] -type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; +type HashBuilder = xxhash_rust::xxh3::Xxh3Builder; impl Cache { + /// Creates a new empty [`Cache`]. pub fn new() -> Self { Self::default() } + /// Gets the text [`Entry`] with the given [`KeyHash`]. pub fn get(&self, key: &KeyHash) -> Option<&Entry> { self.entries.get(key) } + /// Allocates a text [`Entry`] if it is not already present in the [`Cache`]. pub fn allocate( &mut self, font_system: &mut cosmic_text::FontSystem, @@ -88,6 +89,9 @@ impl Cache { (hash, self.entries.get_mut(&hash).unwrap()) } + /// Trims the [`Cache`]. + /// + /// This will clear the sections of text that have not been used since the last `trim`. pub fn trim(&mut self) { self.entries .retain(|key, _| self.recently_used.contains(key)); @@ -99,13 +103,20 @@ impl Cache { } } +/// A cache key representing a section of text. #[derive(Debug, Clone, Copy)] pub struct Key<'a> { + /// The content of the text. pub content: &'a str, + /// The size of the text. pub size: f32, + /// The line height of the text. pub line_height: f32, + /// The [`Font`] of the text. pub font: Font, + /// The bounds of the text. pub bounds: Size, + /// The shaping strategy of the text. pub shaping: text::Shaping, } @@ -123,10 +134,14 @@ impl Key<'_> { } } +/// The hash of a [`Key`]. pub type KeyHash = u64; +/// A cache entry. #[allow(missing_debug_implementations)] pub struct Entry { + /// The buffer of text, ready for drawing. pub buffer: cosmic_text::Buffer, + /// The minimum bounds of the text. pub min_bounds: Size, } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs new file mode 100644 index 00000000..d5262ae8 --- /dev/null +++ b/graphics/src/text/editor.rs @@ -0,0 +1,779 @@ +//! Draw and edit text. +use crate::core::text::editor::{ + self, Action, Cursor, Direction, Edit, Motion, +}; +use crate::core::text::highlighter::{self, Highlighter}; +use crate::core::text::LineHeight; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; +use crate::text; + +use cosmic_text::Edit as _; + +use std::fmt; +use std::sync::{self, Arc}; + +/// A multi-line text editor. +#[derive(Debug, PartialEq)] +pub struct Editor(Option<Arc<Internal>>); + +struct Internal { + editor: cosmic_text::Editor, + font: Font, + bounds: Size, + topmost_line_changed: Option<usize>, + version: text::Version, +} + +impl Editor { + /// Creates a new empty [`Editor`]. + pub fn new() -> Self { + Self::default() + } + + /// Returns the buffer of the [`Editor`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + self.internal().editor.buffer() + } + + /// Creates a [`Weak`] reference to the [`Editor`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let editor = self.internal(); + + Weak { + raw: Arc::downgrade(editor), + bounds: editor.bounds, + } + } + + fn internal(&self) -> &Arc<Internal> { + self.0 + .as_ref() + .expect("Editor should always be initialized") + } +} + +impl editor::Editor for Editor { + type Font = Font; + + fn with_text(text: &str) -> Self { + let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + buffer.set_text( + font_system.raw(), + text, + cosmic_text::Attrs::new(), + cosmic_text::Shaping::Advanced, + ); + + Editor(Some(Arc::new(Internal { + editor: cosmic_text::Editor::new(buffer), + version: font_system.version(), + ..Default::default() + }))) + } + + fn line(&self, index: usize) -> Option<&str> { + self.buffer() + .lines + .get(index) + .map(cosmic_text::BufferLine::text) + } + + fn line_count(&self) -> usize { + self.buffer().lines.len() + } + + fn selection(&self) -> Option<String> { + self.internal().editor.copy_selection() + } + + fn cursor(&self) -> editor::Cursor { + let internal = self.internal(); + + let cursor = internal.editor.cursor(); + let buffer = internal.editor.buffer(); + + match internal.editor.select_opt() { + Some(selection) => { + let (start, end) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + let line_height = buffer.metrics().line_height; + let selected_lines = end.line - start.line + 1; + + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: (visual_line as i32 + visual_lines_offset) + as f32 + * line_height, + height: line_height, + }) + } else { + None + } + }) + .collect(); + + Cursor::Selection(regions) + } + _ => { + let line_height = buffer.metrics().line_height; + + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); + + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (visual_line, offset) = lines + .find_map(|(i, line)| { + let start = line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_before_start = start > cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => cursor.index < end, + }; + + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| cursor.index > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + Some((i, offset)) + } else { + None + } + }) + .unwrap_or(( + layout.len().saturating_sub(1), + layout.last().map(|line| line.w).unwrap_or(0.0), + )); + + Cursor::Caret(Point::new( + offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height, + )) + } + } + } + + fn cursor_position(&self) -> (usize, usize) { + let cursor = self.internal().editor.cursor(); + + (cursor.line, cursor.index) + } + + fn perform(&mut self, action: Action) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let editor = &mut internal.editor; + + match action { + // Motion events + Action::Move(motion) => { + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + let (left, right) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + editor.set_select_opt(None); + + match motion { + // These motions are performed as-is even when a selection + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + motion_to_action(motion), + ); + } + // Other motions simply move the cursor to one end of the selection + _ => editor.set_cursor(match motion.direction() { + Direction::Left => left, + Direction::Right => right, + }), + } + } else { + editor.action(font_system.raw(), motion_to_action(motion)); + } + } + + // Selection events + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.select_opt().is_none() { + editor.set_select_opt(Some(cursor)); + } + + editor.action(font_system.raw(), motion_to_action(motion)); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } + Action::SelectWord => { + use unicode_segmentation::UnicodeSegmentation; + + let cursor = editor.cursor(); + + if let Some(line) = editor.buffer().lines.get(cursor.line) { + let (start, end) = + UnicodeSegmentation::unicode_word_indices(line.text()) + // Split words with dots + .flat_map(|(i, word)| { + word.split('.').scan(i, |current, word| { + let start = *current; + *current += word.len() + 1; + + Some((start, word)) + }) + }) + // Turn words into ranges + .map(|(i, word)| (i, i + word.len())) + // Find the word at cursor + .find(|&(start, end)| { + start <= cursor.index && cursor.index < end + }) + // Cursor is not in a word. Let's select its punctuation cluster. + .unwrap_or_else(|| { + let start = line.text()[..cursor.index] + .char_indices() + .rev() + .take_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i) + .last() + .unwrap_or(cursor.index); + + let end = line.text()[cursor.index..] + .char_indices() + .skip_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i + cursor.index) + .next() + .unwrap_or(cursor.index); + + (start, end) + }); + + if start != end { + editor.set_cursor(cosmic_text::Cursor { + index: start, + ..cursor + }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: end, + ..cursor + })); + } + } + } + Action::SelectLine => { + let cursor = editor.cursor(); + + if let Some(line_length) = editor + .buffer() + .lines + .get(cursor.line) + .map(|line| line.text().len()) + { + editor + .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: line_length, + ..cursor + })); + } + } + + // Editing events + Action::Edit(edit) => { + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection = editor.select_opt().unwrap_or(cursor); + + internal.topmost_line_changed = + Some(cursor.min(selection).line); + } + + // Mouse events + Action::Click(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } + Action::Scroll { lines } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } + } + + self.0 = Some(Arc::new(internal)); + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn update( + &mut self, + new_bounds: Size, + new_font: Font, + new_size: Pixels, + new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, + ) { + let editor = + self.0.take().expect("Editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + if font_system.version() != internal.version { + log::trace!("Updating `FontSystem` of `Editor`..."); + + for line in internal.editor.buffer_mut().lines.iter_mut() { + line.reset(); + } + + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); + } + + if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); + + for line in internal.editor.buffer_mut().lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + text::to_attributes(new_font), + )); + } + + internal.font = new_font; + internal.topmost_line_changed = Some(0); + } + + let metrics = internal.editor.buffer().metrics(); + let new_line_height = new_line_height.to_absolute(new_size); + + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + log::trace!("Updating `Metrics` of `Editor`..."); + + internal.editor.buffer_mut().set_metrics( + font_system.raw(), + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ); + } + + if new_bounds != internal.bounds { + log::trace!("Updating size of `Editor`..."); + + internal.editor.buffer_mut().set_size( + font_system.raw(), + new_bounds.width, + new_bounds.height, + ); + + internal.bounds = new_bounds; + } + + if let Some(topmost_line_changed) = internal.topmost_line_changed.take() + { + log::trace!( + "Notifying highlighter of line change: {topmost_line_changed}" + ); + + new_highlighter.change_line(topmost_line_changed); + } + + internal.editor.shape_as_needed(font_system.raw()); + + self.0 = Some(Arc::new(internal)); + } + + fn highlight<H: Highlighter>( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, + ) { + let internal = self.internal(); + let buffer = internal.editor.buffer(); + + let mut window = buffer.scroll() + buffer.visible_lines(); + + let last_visible_line = buffer + .lines + .iter() + .enumerate() + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(i) + } + }) + .unwrap_or(buffer.lines.len().saturating_sub(1)); + + let current_line = highlighter.current_line(); + + if current_line > last_visible_line { + return; + } + + let editor = + self.0.take().expect("Editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let attributes = text::to_attributes(font); + + for line in &mut internal.editor.buffer_mut().lines + [current_line..=last_visible_line] + { + let mut list = cosmic_text::AttrsList::new(attributes); + + for (range, highlight) in highlighter.highlight_line(line.text()) { + let format = format_highlight(&highlight); + + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } + } + + let _ = line.set_attrs_list(list); + } + + internal.editor.shape_as_needed(font_system.raw()); + + self.0 = Some(Arc::new(internal)); + } +} + +impl Default for Editor { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.bounds == other.bounds + && self.editor.buffer().metrics() == other.editor.buffer().metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty( + cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }, + )), + font: Font::default(), + bounds: Size::ZERO, + topmost_line_changed: None, + version: text::Version::default(), + } + } +} + +impl fmt::Debug for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Internal") + .field("font", &self.font) + .field("bounds", &self.bounds) + .finish() + } +} + +/// A weak reference to an [`Editor`]. +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak<Internal>, + /// The bounds of the [`Editor`]. + pub bounds: Size, +} + +impl Weak { + /// Tries to update the reference into an [`Editor`]. + pub fn upgrade(&self) -> Option<Editor> { + self.raw.upgrade().map(Some).map(Editor) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} + +fn highlight_line( + line: &cosmic_text::BufferLine, + from: usize, + to: usize, +) -> impl Iterator<Item = (f32, f32)> + '_ { + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + layout.iter().map(move |visual_line| { + let start = visual_line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = visual_line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let range = start.max(from)..end.min(to); + + if range.is_empty() { + (0.0, 0.0) + } else if range.start == start && range.end == end { + (0.0, visual_line.w) + } else { + let first_glyph = visual_line + .glyphs + .iter() + .position(|glyph| range.start <= glyph.start) + .unwrap_or(0); + + let mut glyphs = visual_line.glyphs.iter(); + + let x = + glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum(); + + let width: f32 = glyphs + .take_while(|glyph| range.end > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + (x, width) + } + }) +} + +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + let visual_lines_before_start: usize = buffer + .lines + .iter() + .take(line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + visual_lines_before_start as i32 - buffer.scroll() +} + +fn motion_to_action(motion: Motion) -> cosmic_text::Action { + match motion { + Motion::Left => cosmic_text::Action::Left, + Motion::Right => cosmic_text::Action::Right, + Motion::Up => cosmic_text::Action::Up, + Motion::Down => cosmic_text::Action::Down, + Motion::WordLeft => cosmic_text::Action::LeftWord, + Motion::WordRight => cosmic_text::Action::RightWord, + Motion::Home => cosmic_text::Action::Home, + Motion::End => cosmic_text::Action::End, + Motion::PageUp => cosmic_text::Action::PageUp, + Motion::PageDown => cosmic_text::Action::PageDown, + Motion::DocumentStart => cosmic_text::Action::BufferStart, + Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + } +} diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index e4350cff..5d027542 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,12 +1,14 @@ +//! Draw paragraphs. use crate::core; use crate::core::alignment; use crate::core::text::{Hit, LineHeight, Shaping, Text}; use crate::core::{Font, Pixels, Point, Size}; -use crate::text::{self, FontSystem}; +use crate::text; use std::fmt; use std::sync::{self, Arc}; +/// A bunch of text. #[derive(Clone, PartialEq)] pub struct Paragraph(Option<Arc<Internal>>); @@ -23,17 +25,50 @@ struct Internal { } impl Paragraph { + /// Creates a new empty [`Paragraph`]. pub fn new() -> Self { Self::default() } - pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self { + /// Returns the buffer of the [`Paragraph`]. + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + /// Creates a [`Weak`] reference to the [`Paragraph`]. + /// + /// This is useful to avoid cloning the [`Paragraph`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + horizontal_alignment: paragraph.horizontal_alignment, + vertical_alignment: paragraph.vertical_alignment, + } + } + + fn internal(&self) -> &Arc<Internal> { + self.0 + .as_ref() + .expect("paragraph should always be initialized") + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn with_text(text: Text<'_, Font>) -> Self { log::trace!("Allocating paragraph: {}", text.content); - let (mut font_system, version) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); let mut buffer = cosmic_text::Buffer::new( - &mut font_system, + font_system.raw(), cosmic_text::Metrics::new( text.size.into(), text.line_height.to_absolute(text.size).into(), @@ -41,13 +76,13 @@ impl Paragraph { ); buffer.set_size( - &mut font_system, + font_system.raw(), text.bounds.width, text.bounds.height, ); buffer.set_text( - &mut font_system, + font_system.raw(), text.content, text::to_attributes(text.font), text::to_shaping(text.shaping), @@ -64,30 +99,11 @@ impl Paragraph { shaping: text.shaping, bounds: text.bounds, min_bounds, - version, + version: font_system.version(), }))) } - pub fn buffer(&self) -> &cosmic_text::Buffer { - &self.internal().buffer - } - - pub fn version(&self) -> text::Version { - self.internal().version - } - - pub fn downgrade(&self) -> Weak { - let paragraph = self.internal(); - - Weak { - raw: Arc::downgrade(paragraph), - min_bounds: paragraph.min_bounds, - horizontal_alignment: paragraph.horizontal_alignment, - vertical_alignment: paragraph.vertical_alignment, - } - } - - pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) { + fn resize(&mut self, new_bounds: Size) { let paragraph = self .0 .take() @@ -95,10 +111,11 @@ impl Paragraph { match Arc::try_unwrap(paragraph) { Ok(mut internal) => { - let (mut font_system, _) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); internal.buffer.set_size( - &mut font_system, + font_system.raw(), new_bounds.width, new_bounds.height, ); @@ -113,55 +130,42 @@ impl Paragraph { // If there is a strong reference somewhere, we recompute the // buffer from scratch - *self = Self::with_text( - Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }, - font_system, - ); + *self = Self::with_text(Text { + content: &internal.content, + bounds: internal.bounds, + size: Pixels(metrics.font_size), + line_height: LineHeight::Absolute(Pixels( + metrics.line_height, + )), + font: internal.font, + horizontal_alignment: internal.horizontal_alignment, + vertical_alignment: internal.vertical_alignment, + shaping: internal.shaping, + }); } } } - fn internal(&self) -> &Arc<Internal> { - self.0 - .as_ref() - .expect("paragraph should always be initialized") - } -} - -impl core::text::Paragraph for Paragraph { - type Font = Font; - - fn content(&self) -> &str { - &self.internal().content - } - - fn text_size(&self) -> Pixels { - Pixels(self.internal().buffer.metrics().font_size) - } - - fn line_height(&self) -> LineHeight { - LineHeight::Absolute(Pixels( - self.internal().buffer.metrics().line_height, - )) - } - - fn font(&self) -> Font { - self.internal().font - } - - fn shaping(&self) -> Shaping { - self.internal().shaping + fn compare(&self, text: Text<'_, Font>) -> core::text::Difference { + let font_system = text::font_system().read().expect("Read font system"); + let paragraph = self.internal(); + let metrics = paragraph.buffer.metrics(); + + if paragraph.version != font_system.version + || paragraph.content != text.content + || metrics.font_size != text.size.0 + || metrics.line_height != text.line_height.to_absolute(text.size).0 + || paragraph.font != text.font + || paragraph.shaping != text.shaping + || paragraph.horizontal_alignment != text.horizontal_alignment + || paragraph.vertical_alignment != text.vertical_alignment + { + core::text::Difference::Shape + } else if paragraph.bounds != text.bounds { + core::text::Difference::Bounds + } else { + core::text::Difference::None + } } fn horizontal_alignment(&self) -> alignment::Horizontal { @@ -172,10 +176,6 @@ impl core::text::Paragraph for Paragraph { self.internal().vertical_alignment } - fn bounds(&self) -> Size { - self.internal().bounds - } - fn min_bounds(&self) -> Size { self.internal().min_bounds } @@ -187,38 +187,43 @@ impl core::text::Paragraph for Paragraph { } fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { + use unicode_segmentation::UnicodeSegmentation; + let run = self.internal().buffer.layout_runs().nth(line)?; // index represents a grapheme, not a glyph // Let's find the first glyph for the given grapheme cluster let mut last_start = None; + let mut last_grapheme_count = 0; let mut graphemes_seen = 0; let glyph = run .glyphs .iter() .find(|glyph| { - if graphemes_seen == index { - return true; - } - if Some(glyph.start) != last_start { + last_grapheme_count = run.text[glyph.start..glyph.end] + .graphemes(false) + .count(); last_start = Some(glyph.start); - graphemes_seen += 1; + graphemes_seen += last_grapheme_count; } - false + graphemes_seen >= index }) .or_else(|| run.glyphs.last())?; - let advance_last = if index == run.glyphs.len() { - glyph.w - } else { + let advance = if index == 0 { 0.0 + } else { + glyph.w + * (1.0 + - graphemes_seen.saturating_sub(index) as f32 + / last_grapheme_count.max(1) as f32) }; Some(Point::new( - glyph.x + glyph.x_offset * glyph.font_size + advance_last, + glyph.x + glyph.x_offset * glyph.font_size + advance, glyph.y - glyph.y_offset * glyph.font_size, )) } @@ -278,15 +283,20 @@ impl Default for Internal { } } +/// A weak reference to a [`Paragraph`]. #[derive(Debug, Clone)] pub struct Weak { raw: sync::Weak<Internal>, + /// The minimum bounds of the [`Paragraph`]. pub min_bounds: Size, + /// The horizontal alignment of the [`Paragraph`]. pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the [`Paragraph`]. pub vertical_alignment: alignment::Vertical, } impl Weak { + /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option<Paragraph> { self.raw.upgrade().map(Some).map(Paragraph) } diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml new file mode 100644 index 00000000..2d108d6f --- /dev/null +++ b/highlighter/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "iced_highlighter" +description = "A syntax highlighter for iced" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +iced_core.workspace = true + +once_cell.workspace = true +syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs new file mode 100644 index 00000000..63f21fc0 --- /dev/null +++ b/highlighter/src/lib.rs @@ -0,0 +1,245 @@ +use iced_core as core; + +use crate::core::text::highlighter::{self, Format}; +use crate::core::{Color, Font}; + +use once_cell::sync::Lazy; +use std::ops::Range; +use syntect::highlighting; +use syntect::parsing; + +static SYNTAXES: Lazy<parsing::SyntaxSet> = + Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines); + +static THEMES: Lazy<highlighting::ThemeSet> = + Lazy::new(highlighting::ThemeSet::load_defaults); + +const LINES_PER_SNAPSHOT: usize = 50; + +pub struct Highlighter { + syntax: &'static parsing::SyntaxReference, + highlighter: highlighting::Highlighter<'static>, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, + current_line: usize, +} + +impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntax = SYNTAXES + .find_syntax_by_token(&settings.extension) + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); + + let highlighter = highlighting::Highlighter::new( + &THEMES.themes[settings.theme.key()], + ); + + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + + Highlighter { + syntax, + highlighter, + caches: vec![(parser, stack)], + current_line: 0, + } + } + + fn update(&mut self, new_settings: &Self::Settings) { + self.syntax = SYNTAXES + .find_syntax_by_token(&new_settings.extension) + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); + + self.highlighter = highlighting::Highlighter::new( + &THEMES.themes[new_settings.theme.key()], + ); + + // Restart the highlighter + self.change_line(0); + } + + fn change_line(&mut self, line: usize) { + let snapshot = line / LINES_PER_SNAPSHOT; + + if snapshot <= self.caches.len() { + self.caches.truncate(snapshot); + self.current_line = snapshot * LINES_PER_SNAPSHOT; + } else { + self.caches.truncate(1); + self.current_line = 0; + } + + let (parser, stack) = + self.caches.last().cloned().unwrap_or_else(|| { + ( + parsing::ParseState::new(self.syntax), + parsing::ScopeStack::new(), + ) + }); + + self.caches.push((parser, stack)); + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { + let (parser, stack) = + self.caches.last().expect("Caches must not be empty"); + + self.caches.push((parser.clone(), stack.clone())); + } + + self.current_line += 1; + + let (parser, stack) = + self.caches.last_mut().expect("Caches must not be empty"); + + let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default(); + + let highlighter = &self.highlighter; + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let _ = stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter.style_mod_for_stack(&stack.scopes), + ), + )) + } + }), + ) + } + + fn current_line(&self) -> usize { + self.current_line + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Settings { + pub theme: Theme, + pub extension: String, +} + +pub struct Highlight(highlighting::StyleModifier); + +impl Highlight { + pub fn color(&self) -> Option<Color> { + self.0.foreground.map(|color| { + Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0) + }) + } + + pub fn font(&self) -> Option<Font> { + None + } + + pub fn to_format(&self) -> Format<Font> { + Format { + color: self.color(), + font: self.font(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + SolarizedDark, + Base16Mocha, + Base16Ocean, + Base16Eighties, + InspiredGitHub, +} + +impl Theme { + pub const ALL: &'static [Self] = &[ + Self::SolarizedDark, + Self::Base16Mocha, + Self::Base16Ocean, + Self::Base16Eighties, + Self::InspiredGitHub, + ]; + + pub fn is_dark(self) -> bool { + match self { + Self::SolarizedDark + | Self::Base16Mocha + | Self::Base16Ocean + | Self::Base16Eighties => true, + Self::InspiredGitHub => false, + } + } + + fn key(self) -> &'static str { + match self { + Theme::SolarizedDark => "Solarized (dark)", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::Base16Ocean => "base16-ocean.dark", + Theme::Base16Eighties => "base16-eighties.dark", + Theme::InspiredGitHub => "InspiredGitHub", + } + } +} + +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Theme::SolarizedDark => write!(f, "Solarized Dark"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::Base16Ocean => write!(f, "Ocean"), + Theme::Base16Eighties => write!(f, "Eighties"), + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), + } + } +} + +pub struct ScopeRangeIterator { + ops: Vec<(usize, parsing::ScopeStackOp)>, + line_length: usize, + index: usize, + last_str_index: usize, +} + +impl Iterator for ScopeRangeIterator { + type Item = (std::ops::Range<usize>, parsing::ScopeStackOp); + + fn next(&mut self) -> Option<Self::Item> { + if self.index > self.ops.len() { + return None; + } + + let next_str_i = if self.index == self.ops.len() { + self.line_length + } else { + self.ops[self.index].0 + }; + + let range = self.last_str_index..next_str_i; + self.last_str_index = next_str_i; + + let op = if self.index == 0 { + parsing::ScopeStackOp::Noop + } else { + self.ops[self.index - 1].1.clone() + }; + + self.index += 1; + Some((range, op)) + } +} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 56e17209..a159978c 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -27,5 +27,4 @@ iced_wgpu.workspace = true iced_wgpu.optional = true log.workspace = true -raw-window-handle.workspace = true thiserror.workspace = true diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs deleted file mode 100644 index 3f229b52..00000000 --- a/renderer/src/backend.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::core::text; -use crate::core::{Font, Point, Size}; -use crate::graphics::backend; - -use std::borrow::Cow; - -#[allow(clippy::large_enum_variant)] -pub enum Backend { - TinySkia(iced_tiny_skia::Backend), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Backend), -} - -macro_rules! delegate { - ($backend:expr, $name:ident, $body:expr) => { - match $backend { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; -} - -impl backend::Text for Backend { - const ICON_FONT: Font = Font::with_name("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{f00c}'; - const ARROW_DOWN_ICON: char = '\u{e800}'; - - fn default_font(&self) -> Font { - delegate!(self, backend, backend.default_font()) - } - - fn default_size(&self) -> f32 { - delegate!(self, backend, backend.default_size()) - } - - fn measure( - &self, - contents: &str, - size: f32, - line_height: text::LineHeight, - font: Font, - bounds: Size, - shaping: text::Shaping, - ) -> Size { - delegate!( - self, - backend, - backend.measure(contents, size, line_height, font, bounds, shaping) - ) - } - - fn hit_test( - &self, - contents: &str, - size: f32, - line_height: text::LineHeight, - font: Font, - bounds: Size, - shaping: text::Shaping, - position: Point, - nearest_only: bool, - ) -> Option<text::Hit> { - delegate!( - self, - backend, - backend.hit_test( - contents, - size, - line_height, - font, - bounds, - shaping, - position, - nearest_only, - ) - ) - } - - fn load_font(&mut self, font: Cow<'static, [u8]>) { - delegate!(self, backend, backend.load_font(font)); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { - delegate!(self, backend, backend.dimensions(handle)) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size<u32> { - delegate!(self, backend, backend.viewport_dimensions(handle)) - } -} diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index d1500089..f10ed048 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -1,9 +1,8 @@ use crate::core::Color; -use crate::graphics::compositor::{Information, SurfaceError}; +use crate::graphics::compositor::{Information, SurfaceError, Window}; use crate::graphics::{Error, Viewport}; use crate::{Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::env; pub enum Compositor<Theme> { @@ -15,7 +14,7 @@ pub enum Compositor<Theme> { pub enum Surface { TinySkia(iced_tiny_skia::window::Surface), #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Surface), + Wgpu(iced_wgpu::window::Surface<'static>), } impl<Theme> crate::graphics::Compositor for Compositor<Theme> { @@ -23,20 +22,18 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { type Renderer = Renderer<Theme>; type Surface = Surface; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new<W: Window + Clone>( settings: Self::Settings, - compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { + compatible_window: W, + ) -> Result<Self, Error> { let candidates = Candidate::list_from_env().unwrap_or(Candidate::default_list()); let mut error = Error::GraphicsAdapterNotFound; for candidate in candidates { - match candidate.build(settings, compatible_window) { - Ok((compositor, renderer)) => { - return Ok((compositor, renderer)) - } + match candidate.build(settings, compatible_window.clone()) { + Ok(compositor) => return Ok(compositor), Err(new_error) => { error = new_error; } @@ -46,9 +43,21 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { Err(error) } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_renderer(&self) -> Self::Renderer { + match self { + Compositor::TinySkia(compositor) => { + Renderer::TinySkia(compositor.create_renderer()) + } + #[cfg(feature = "wgpu")] + Compositor::Wgpu(compositor) => { + Renderer::Wgpu(compositor.create_renderer()) + } + } + } + + fn create_surface<W: Window + Clone>( &mut self, - window: &W, + window: W, width: u32, height: u32, ) -> Surface { @@ -216,28 +225,26 @@ impl Candidate { ) } - fn build<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( + fn build<Theme, W: Window>( self, settings: Settings, - _compatible_window: Option<&W>, - ) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> { + _compatible_window: W, + ) -> Result<Compositor<Theme>, Error> { match self { Self::TinySkia => { - let (compositor, backend) = - iced_tiny_skia::window::compositor::new(); + let compositor = iced_tiny_skia::window::compositor::new( + iced_tiny_skia::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + }, + _compatible_window, + ); - Ok(( - Compositor::TinySkia(compositor), - Renderer::TinySkia(iced_tiny_skia::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::TinySkia(compositor)) } #[cfg(feature = "wgpu")] Self::Wgpu => { - let (compositor, backend) = iced_wgpu::window::compositor::new( + let compositor = iced_wgpu::window::compositor::new( iced_wgpu::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, @@ -247,14 +254,7 @@ impl Candidate { _compatible_window, )?; - Ok(( - Compositor::Wgpu(compositor), - Renderer::Wgpu(iced_wgpu::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::Wgpu(compositor)) } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 7594d532..f2acfa00 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,6 +1,9 @@ #![forbid(rust_2018_idioms)] #![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[cfg(feature = "wgpu")] +pub use iced_wgpu as wgpu; + pub mod compositor; #[cfg(feature = "geometry")] @@ -19,9 +22,8 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::text::{self, Text}; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Vector, -}; +use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; +use crate::graphics::text::Editor; use crate::graphics::text::Paragraph; use crate::graphics::Mesh; @@ -149,6 +151,7 @@ impl<T> core::Renderer for Renderer<T> { impl<T> text::Renderer for Renderer<T> { type Font = Font; type Paragraph = Paragraph; + type Editor = Editor; const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON; @@ -163,36 +166,35 @@ impl<T> text::Renderer for Renderer<T> { delegate!(self, renderer, renderer.default_size()) } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph { - delegate!(self, renderer, renderer.create_paragraph(text)) + fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + delegate!(self, renderer, renderer.load_font(bytes)); } - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, + fn fill_paragraph( + &mut self, + paragraph: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, ) { delegate!( self, renderer, - renderer.resize_paragraph(paragraph, new_bounds) + renderer.fill_paragraph(paragraph, position, color, clip_bounds) ); } - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - delegate!(self, renderer, renderer.load_font(bytes)); - } - - fn fill_paragraph( + fn fill_editor( &mut self, - text: &Self::Paragraph, + editor: &Self::Editor, position: Point, color: Color, + clip_bounds: Rectangle, ) { delegate!( self, renderer, - renderer.fill_paragraph(text, position, color) + renderer.fill_editor(editor, position, color, clip_bounds) ); } @@ -201,8 +203,13 @@ impl<T> text::Renderer for Renderer<T> { text: Text<'_, Self::Font>, position: Point, color: Color, + clip_bounds: Rectangle, ) { - delegate!(self, renderer, renderer.fill_text(text, position, color)); + delegate!( + self, + renderer, + renderer.fill_text(text, position, color, clip_bounds) + ); } } @@ -210,18 +217,26 @@ impl<T> text::Renderer for Renderer<T> { impl<T> crate::core::image::Renderer for Renderer<T> { type Handle = crate::core::image::Handle; - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { + fn dimensions( + &self, + handle: &crate::core::image::Handle, + ) -> core::Size<u32> { delegate!(self, renderer, renderer.dimensions(handle)) } - fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw(handle, bounds)); + fn draw( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, + ) { + delegate!(self, renderer, renderer.draw(handle, filter_method, bounds)); } } #[cfg(feature = "svg")] impl<T> crate::core::svg::Renderer for Renderer<T> { - fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> { + fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> { delegate!(self, renderer, renderer.dimensions(handle)) } @@ -247,6 +262,7 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> { crate::Geometry::TinySkia(primitive) => { renderer.draw_primitive(primitive); } + #[cfg(feature = "wgpu")] crate::Geometry::Wgpu(_) => unreachable!(), } } @@ -265,3 +281,23 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> { } } } + +#[cfg(feature = "wgpu")] +impl<T> iced_wgpu::primitive::pipeline::Renderer for Renderer<T> { + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl wgpu::primitive::pipeline::Primitive, + ) { + match self { + Self::TinySkia(_renderer) => { + log::warn!( + "Custom shader primitive is unavailable with tiny-skia." + ); + } + Self::Wgpu(renderer) => { + renderer.draw_pipeline_primitive(bounds, primitive); + } + } + } +} diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs deleted file mode 100644 index 6c0c2a83..00000000 --- a/renderer/src/widget.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(feature = "canvas")] -pub mod canvas; - -#[cfg(feature = "canvas")] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -pub use qr_code::QRCode; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d19aedd3..8089d545 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -12,6 +12,7 @@ keywords.workspace = true [features] debug = [] +multi-window = [] [dependencies] iced_core.workspace = true diff --git a/runtime/src/command.rs b/runtime/src/command.rs index cd4c51ff..f70da915 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -4,8 +4,11 @@ mod action; pub use action::Action; use crate::core::widget; +use crate::futures::futures; use crate::futures::MaybeSend; +use futures::channel::mpsc; +use futures::Stream; use std::fmt; use std::future::Future; @@ -40,14 +43,24 @@ impl<T> Command<T> { /// Creates a [`Command`] that performs the action of the given future. pub fn perform<A>( - future: impl Future<Output = T> + 'static + MaybeSend, - f: impl FnOnce(T) -> A + 'static + MaybeSend, - ) -> Command<A> { - use iced_futures::futures::FutureExt; + future: impl Future<Output = A> + 'static + MaybeSend, + f: impl FnOnce(A) -> T + 'static + MaybeSend, + ) -> Command<T> { + use futures::FutureExt; Command::single(Action::Future(Box::pin(future.map(f)))) } + /// Creates a [`Command`] that runs the given stream to completion. + pub fn run<A>( + stream: impl Stream<Item = A> + 'static + MaybeSend, + f: impl Fn(A) -> T + 'static + MaybeSend, + ) -> Command<T> { + use futures::StreamExt; + + Command::single(Action::Stream(Box::pin(stream.map(f)))) + } + /// Creates a [`Command`] that performs the actions of all the given /// commands. /// @@ -106,3 +119,23 @@ impl<T> fmt::Debug for Command<T> { command.fmt(f) } } + +/// Creates a [`Command`] that produces the `Message`s published from a [`Future`] +/// to an [`mpsc::Sender`] with the given bounds. +pub fn channel<Fut, Message>( + size: usize, + f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, +) -> Command<Message> +where + Fut: Future<Output = ()> + MaybeSend + 'static, + Message: 'static + MaybeSend, +{ + use futures::future; + use futures::stream::{self, StreamExt}; + + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|_| future::ready(None)); + + Command::single(Action::Stream(Box::pin(stream::select(receiver, runner)))) +} diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index 6c74f0ef..cb0936df 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -18,6 +18,11 @@ pub enum Action<T> { /// [`Future`]: iced_futures::BoxFuture Future(iced_futures::BoxFuture<T>), + /// Run a [`Stream`] to completion. + /// + /// [`Stream`]: iced_futures::BoxStream + Stream(iced_futures::BoxStream<T>), + /// Run a clipboard action. Clipboard(clipboard::Action<T>), @@ -52,10 +57,11 @@ impl<T> Action<T> { A: 'static, T: 'static, { - use iced_futures::futures::FutureExt; + use iced_futures::futures::{FutureExt, StreamExt}; match self { Self::Future(future) => Action::Future(Box::pin(future.map(f))), + Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(window) => Action::Window(window.map(f)), Self::System(system) => Action::System(system.map(f)), @@ -74,10 +80,13 @@ impl<T> fmt::Debug for Action<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Future(_) => write!(f, "Action::Future"), + Self::Stream(_) => write!(f, "Action::Stream"), Self::Clipboard(action) => { write!(f, "Action::Clipboard({action:?})") } - Self::Window(action) => write!(f, "Action::Window({action:?})"), + Self::Window(action) => { + write!(f, "Action::Window({action:?})") + } Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"), diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 29e94d65..03906f45 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -26,6 +26,9 @@ pub mod system; pub mod user_interface; pub mod window; +#[cfg(feature = "multi-window")] +pub mod multi_window; + // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs new file mode 100644 index 00000000..cf778a20 --- /dev/null +++ b/runtime/src/multi_window.rs @@ -0,0 +1,6 @@ +//! A multi-window application. +pub mod program; +pub mod state; + +pub use program::Program; +pub use state::State; diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs new file mode 100644 index 00000000..591b3e9a --- /dev/null +++ b/runtime/src/multi_window/program.rs @@ -0,0 +1,32 @@ +//! Build interactive programs using The Elm Architecture. +use crate::core::text; +use crate::core::window; +use crate::core::{Element, Renderer}; +use crate::Command; + +/// The core of a user interface for a multi-window application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer + text::Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs new file mode 100644 index 00000000..49f72c39 --- /dev/null +++ b/runtime/src/multi_window/state.rs @@ -0,0 +1,280 @@ +//! The internal state of a multi-window [`Program`]. +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; +use crate::core::{Clipboard, Size}; +use crate::user_interface::{self, UserInterface}; +use crate::{Command, Debug, Program}; + +/// The execution state of a multi-window [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State<P> +where + P: Program + 'static, +{ + program: P, + caches: Option<Vec<user_interface::Cache>>, + queued_events: Vec<Event>, + queued_messages: Vec<P::Message>, + mouse_interaction: mouse::Interaction, +} + +impl<P> State<P> +where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + program: P, + bounds: Size, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let user_interface = build_user_interface( + &program, + user_interface::Cache::default(), + renderer, + bounds, + debug, + ); + + let caches = Some(vec![user_interface.into_cache()]); + + State { + program, + caches, + queued_events: Vec::new(), + queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::Idle, + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns a list containing the instances of [`Event`] that were not + /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// after updating it, only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor: mouse::Cursor, + renderer: &mut P::Renderer, + theme: &<P::Renderer as iced_core::Renderer>::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec<Event>, Option<Command<P::Message>>) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let uncaptured_events = user_interfaces.iter_mut().fold( + vec![], + |mut uncaptured_events, ui| { + let (_, event_statuses) = ui.update( + &self.queued_events, + cursor, + renderer, + clipboard, + &mut messages, + ); + + uncaptured_events.extend( + self.queued_events + .iter() + .zip(event_statuses) + .filter_map(|(event, status)| { + matches!(status, event::Status::Ignored) + .then_some(event) + }) + .cloned(), + ); + uncaptured_events + }, + ); + + self.queued_events.clear(); + messages.append(&mut self.queued_messages); + debug.event_processing_finished(); + + let commands = if messages.is_empty() { + debug.draw_started(); + + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + None + } else { + let temp_caches = user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + drop(user_interfaces); + + let commands = Command::batch(messages.into_iter().map(|msg| { + debug.log_message(&msg); + + debug.update_started(); + let command = self.program.update(msg); + debug.update_finished(); + + command + })); + + let mut user_interfaces = build_user_interfaces( + &self.program, + temp_caches, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + Some(commands) + }; + + (uncaptured_events, commands) + } + + /// Applies widget [`Operation`]s to the [`State`]. + pub fn operate( + &mut self, + renderer: &mut P::Renderer, + operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>, + bounds: Size, + debug: &mut Debug, + ) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + for operation in operations { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for ui in &mut user_interfaces { + ui.operate(renderer, operation.as_mut()); + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + self.queued_messages.push(message); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + }; + } + } + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + } +} + +fn build_user_interfaces<'a, P: Program>( + program: &'a P, + mut caches: Vec<user_interface::Cache>, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> Vec<UserInterface<'a, P::Message, P::Renderer>> { + caches + .drain(..) + .map(|cache| { + build_user_interface(program, cache, renderer, size, debug) + }) + .collect() +} + +fn build_user_interface<'a, P: Program>( + program: &'a P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 062ccc72..4256efb7 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -4,7 +4,9 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget; -use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size}; +use crate::core::{ + Clipboard, Event, Layout, Point, Rectangle, Shell, Size, Vector, +}; /// An overlay container that displays nested overlays #[allow(missing_debug_implementations)] @@ -33,19 +35,18 @@ where &mut self, renderer: &Renderer, bounds: Size, - position: Point, + _position: Point, + translation: Vector, ) -> layout::Node { fn recurse<Message, Renderer>( element: &mut overlay::Element<'_, Message, Renderer>, renderer: &Renderer, bounds: Size, - position: Point, + translation: Vector, ) -> layout::Node where Renderer: renderer::Renderer, { - let translation = position - Point::ORIGIN; - let node = element.layout(renderer, bounds, translation); if let Some(mut nested) = @@ -55,7 +56,7 @@ where node.size(), vec![ node, - recurse(&mut nested, renderer, bounds, position), + recurse(&mut nested, renderer, bounds, translation), ], ) } else { @@ -63,7 +64,7 @@ where } } - recurse(&mut self.overlay, renderer, bounds, position) + recurse(&mut self.overlay, renderer, bounds, translation) } /// Draws the [`Nested`] overlay using the associated `Renderer`. diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index dae9e0ac..3594ac18 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,7 +5,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget; use crate::core::window; -use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::core::{ + Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, +}; use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -199,7 +201,8 @@ where let bounds = self.bounds; let mut overlay = manual_overlay.as_mut().unwrap(); - let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN); + let mut layout = + overlay.layout(renderer, bounds, Point::ORIGIN, Vector::ZERO); let mut event_statuses = Vec::new(); for event in events.iter().cloned() { @@ -253,8 +256,12 @@ where overlay = manual_overlay.as_mut().unwrap(); shell.revalidate_layout(|| { - layout = - overlay.layout(renderer, bounds, Point::ORIGIN); + layout = overlay.layout( + renderer, + bounds, + Point::ORIGIN, + Vector::ZERO, + ); }); } @@ -448,7 +455,12 @@ where .map(overlay::Nested::new) { let overlay_layout = self.overlay.take().unwrap_or_else(|| { - overlay.layout(renderer, self.bounds, Point::ORIGIN) + overlay.layout( + renderer, + self.bounds, + Point::ORIGIN, + Vector::ZERO, + ) }); let cursor = if cursor @@ -566,8 +578,12 @@ where .map(overlay::Nested::new) { if self.overlay.is_none() { - self.overlay = - Some(overlay.layout(renderer, self.bounds, Point::ORIGIN)); + self.overlay = Some(overlay.layout( + renderer, + self.bounds, + Point::ORIGIN, + Vector::ZERO, + )); } overlay.operate( diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 41816967..2136d64d 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -8,95 +8,135 @@ pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; -use crate::core::Size; +use crate::core::window::{ + Event, Icon, Id, Level, Mode, Settings, UserAttention, +}; +use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::Subscription; /// Subscribes to the frames of the window of the running application. /// /// The resulting [`Subscription`] will produce items at a rate equal to the -/// refresh rate of the window. Note that this rate may be variable, as it is +/// refresh rate of the first application window. Note that this rate may be variable, as it is /// normally managed by the graphics driver and/or the OS. /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription<Instant> { event::listen_raw(|event, _status| match event { - iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at), + crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), _ => None, }) } -/// Closes the current window and exits the application. -pub fn close<Message>() -> Command<Message> { - Command::single(command::Action::Window(Action::Close)) +/// Spawns a new window with the given `settings`. +/// +/// Returns the new window [`Id`] alongside the [`Command`]. +pub fn spawn<Message>(settings: Settings) -> (Id, Command<Message>) { + let id = Id::unique(); + + ( + id, + Command::single(command::Action::Window(Action::Spawn(id, settings))), + ) +} + +/// Closes the window with `id`. +pub fn close<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::Close(id))) } /// Begins dragging the window while the left mouse button is held. -pub fn drag<Message>() -> Command<Message> { - Command::single(command::Action::Window(Action::Drag)) +pub fn drag<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. -pub fn resize<Message>(new_size: Size<u32>) -> Command<Message> { - Command::single(command::Action::Window(Action::Resize(new_size))) +pub fn resize<Message>(id: Id, new_size: Size) -> Command<Message> { + Command::single(command::Action::Window(Action::Resize(id, new_size))) } -/// Fetches the current window size in logical dimensions. +/// Fetches the window's size in logical dimensions. pub fn fetch_size<Message>( - f: impl FnOnce(Size<u32>) -> Message + 'static, + id: Id, + f: impl FnOnce(Size) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchSize(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) +} + +/// Fetches if the window is maximized. +pub fn fetch_maximized<Message>( + id: Id, + f: impl FnOnce(bool) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchMaximized( + id, + Box::new(f), + ))) } /// Maximizes the window. -pub fn maximize<Message>(maximized: bool) -> Command<Message> { - Command::single(command::Action::Window(Action::Maximize(maximized))) +pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Maximize(id, maximized))) +} + +/// Fetches if the window is minimized. +pub fn fetch_minimized<Message>( + id: Id, + f: impl FnOnce(Option<bool>) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchMinimized( + id, + Box::new(f), + ))) } -/// Minimes the window. -pub fn minimize<Message>(minimized: bool) -> Command<Message> { - Command::single(command::Action::Window(Action::Minimize(minimized))) +/// Minimizes the window. +pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Minimize(id, minimized))) } -/// Moves a window to the given logical coordinates. -pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> { - Command::single(command::Action::Window(Action::Move { x, y })) +/// Moves the window to the given logical coordinates. +pub fn move_to<Message>(id: Id, position: Point) -> Command<Message> { + Command::single(command::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. -pub fn change_mode<Message>(mode: Mode) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeMode(mode))) +pub fn change_mode<Message>(id: Id, mode: Mode) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeMode(id, mode))) } /// Fetches the current [`Mode`] of the window. pub fn fetch_mode<Message>( + id: Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchMode(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) } /// Toggles the window to maximized or back. -pub fn toggle_maximize<Message>() -> Command<Message> { - Command::single(command::Action::Window(Action::ToggleMaximize)) +pub fn toggle_maximize<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations<Message>() -> Command<Message> { - Command::single(command::Action::Window(Action::ToggleDecorations)) +pub fn toggle_decorations<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleDecorations(id))) } -/// Request user attention to the window, this has no effect if the application +/// Request user attention to the window. This has no effect if the application /// is already focused. How requesting for user attention manifests is platform dependent, /// see [`UserAttention`] for details. /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. pub fn request_user_attention<Message>( + id: Id, user_attention: Option<UserAttention>, ) -> Command<Message> { Command::single(command::Action::Window(Action::RequestUserAttention( + id, user_attention, ))) } @@ -107,30 +147,36 @@ pub fn request_user_attention<Message>( /// This [`Command`] steals input focus from other applications. Do not use this method unless /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. -pub fn gain_focus<Message>() -> Command<Message> { - Command::single(command::Action::Window(Action::GainFocus)) +pub fn gain_focus<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level<Message>(level: Level) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeLevel(level))) +pub fn change_level<Message>(id: Id, level: Level) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } -/// Fetches an identifier unique to the window. +/// Fetches an identifier unique to the window, provided by the underlying windowing system. This is +/// not to be confused with [`Id`]. pub fn fetch_id<Message>( + id: Id, f: impl FnOnce(u64) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) } /// Changes the [`Icon`] of the window. -pub fn change_icon<Message>(icon: Icon) -> Command<Message> { - Command::single(command::Action::Window(Action::ChangeIcon(icon))) +pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) } /// Captures a [`Screenshot`] from the window. pub fn screenshot<Message>( + id: Id, f: impl FnOnce(Screenshot) -> Message + Send + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) + Command::single(command::Action::Window(Action::Screenshot( + id, + Box::new(f), + ))) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index b6964e36..8b532569 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,5 +1,5 @@ -use crate::core::window::{Icon, Level, Mode, UserAttention}; -use crate::core::Size; +use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; +use crate::core::{Point, Size}; use crate::futures::MaybeSend; use crate::window::Screenshot; @@ -7,43 +7,51 @@ use std::fmt; /// An operation to be performed on some window. pub enum Action<T> { - /// Close the current window and exits the application. - Close, + /// Spawns a new window with some [`Settings`]. + Spawn(Id, Settings), + /// Close the window and exits the application. + Close(Id), /// Move the window with the left mouse button until the button is /// released. /// /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. - Drag, - /// Resize the window. - Resize(Size<u32>), - /// Fetch the current size of the window. - FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>), + Drag(Id), + /// Resize the window to the given logical dimensions. + Resize(Id, Size), + /// Fetch the current logical dimensions of the window. + FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>), + /// Fetch if the current window is maximized or not. + /// + /// ## Platform-specific + /// - **iOS / Android / Web:** Unsupported. + FetchMaximized(Id, Box<dyn FnOnce(bool) -> T + 'static>), /// Set the window to maximized or back - Maximize(bool), + Maximize(Id, bool), + /// Fetch if the current window is minimized or not. + /// + /// ## Platform-specific + /// - **Wayland:** Always `None`. + /// - **iOS / Android / Web:** Unsupported. + FetchMinimized(Id, Box<dyn FnOnce(Option<bool>) -> T + 'static>), /// Set the window to minimized or back - Minimize(bool), - /// Move the window. + Minimize(Id, bool), + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. - Move { - /// The new logical x location of the window - x: i32, - /// The new logical y location of the window - y: i32, - }, + Move(Id, Point), /// Change the [`Mode`] of the window. - ChangeMode(Mode), + ChangeMode(Id, Mode), /// Fetch the current [`Mode`] of the window. - FetchMode(Box<dyn FnOnce(Mode) -> T + 'static>), + FetchMode(Id, Box<dyn FnOnce(Mode) -> T + 'static>), /// Toggle the window to maximized or back - ToggleMaximize, + ToggleMaximize(Id), /// Toggle whether window has decorations. /// /// ## Platform-specific /// - **X11:** Not implemented. /// - **Web:** Unsupported. - ToggleDecorations, + ToggleDecorations(Id), /// Request user attention to the window, this has no effect if the application /// is already focused. How requesting for user attention manifests is platform dependent, /// see [`UserAttention`] for details. @@ -57,7 +65,7 @@ pub enum Action<T> { /// - **macOS:** `None` has no effect. /// - **X11:** Requests for user attention must be manually cleared. /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Option<UserAttention>), + RequestUserAttention(Id, Option<UserAttention>), /// Bring the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// @@ -68,11 +76,11 @@ pub enum Action<T> { /// ## Platform-specific /// /// - **Web / Wayland:** Unsupported. - GainFocus, + GainFocus(Id), /// Change the window [`Level`]. - ChangeLevel(Level), - /// Fetch an identifier unique to the window. - FetchId(Box<dyn FnOnce(u64) -> T + 'static>), + ChangeLevel(Id, Level), + /// Fetch the raw identifier unique to the window. + FetchId(Id, Box<dyn FnOnce(u64) -> T + 'static>), /// Change the window [`Icon`]. /// /// On Windows and X11, this is typically the small icon in the top-left @@ -87,9 +95,9 @@ pub enum Action<T> { /// /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Icon), + ChangeIcon(Id, Icon), /// Screenshot the viewport of the window. - Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>), + Screenshot(Id, Box<dyn FnOnce(Screenshot) -> T + 'static>), } impl<T> Action<T> { @@ -102,29 +110,41 @@ impl<T> Action<T> { T: 'static, { match self { - Self::Close => Action::Close, - Self::Drag => Action::Drag, - Self::Resize(size) => Action::Resize(size), - Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), - Self::Maximize(maximized) => Action::Maximize(maximized), - Self::Minimize(minimized) => Action::Minimize(minimized), - Self::Move { x, y } => Action::Move { x, y }, - Self::ChangeMode(mode) => Action::ChangeMode(mode), - Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))), - Self::ToggleMaximize => Action::ToggleMaximize, - Self::ToggleDecorations => Action::ToggleDecorations, - Self::RequestUserAttention(attention_type) => { - Action::RequestUserAttention(attention_type) - } - Self::GainFocus => Action::GainFocus, - Self::ChangeLevel(level) => Action::ChangeLevel(level), - Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), - Self::ChangeIcon(icon) => Action::ChangeIcon(icon), - Self::Screenshot(tag) => { - Action::Screenshot(Box::new(move |screenshot| { - f(tag(screenshot)) - })) + Self::Spawn(id, settings) => Action::Spawn(id, settings), + Self::Close(id) => Action::Close(id), + Self::Drag(id) => Action::Drag(id), + Self::Resize(id, size) => Action::Resize(id, size), + Self::FetchSize(id, o) => { + Action::FetchSize(id, Box::new(move |s| f(o(s)))) } + Self::FetchMaximized(id, o) => { + Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) + } + Self::Maximize(id, maximized) => Action::Maximize(id, maximized), + Self::FetchMinimized(id, o) => { + Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) + } + Self::Minimize(id, minimized) => Action::Minimize(id, minimized), + Self::Move(id, position) => Action::Move(id, position), + Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), + Self::FetchMode(id, o) => { + Action::FetchMode(id, Box::new(move |s| f(o(s)))) + } + Self::ToggleMaximize(id) => Action::ToggleMaximize(id), + Self::ToggleDecorations(id) => Action::ToggleDecorations(id), + Self::RequestUserAttention(id, attention_type) => { + Action::RequestUserAttention(id, attention_type) + } + Self::GainFocus(id) => Action::GainFocus(id), + Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), + Self::FetchId(id, o) => { + Action::FetchId(id, Box::new(move |s| f(o(s)))) + } + Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon), + Self::Screenshot(id, tag) => Action::Screenshot( + id, + Box::new(move |screenshot| f(tag(screenshot))), + ), } } } @@ -132,35 +152,52 @@ impl<T> Action<T> { impl<T> fmt::Debug for Action<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Close => write!(f, "Action::Close"), - Self::Drag => write!(f, "Action::Drag"), - Self::Resize(size) => write!(f, "Action::Resize({size:?})"), - Self::FetchSize(_) => write!(f, "Action::FetchSize"), - Self::Maximize(maximized) => { - write!(f, "Action::Maximize({maximized})") - } - Self::Minimize(minimized) => { - write!(f, "Action::Minimize({minimized}") - } - Self::Move { x, y } => { - write!(f, "Action::Move {{ x: {x}, y: {y} }}") - } - Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), - Self::FetchMode(_) => write!(f, "Action::FetchMode"), - Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"), - Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"), - Self::RequestUserAttention(_) => { - write!(f, "Action::RequestUserAttention") - } - Self::GainFocus => write!(f, "Action::GainFocus"), - Self::ChangeLevel(level) => { - write!(f, "Action::ChangeLevel({level:?})") - } - Self::FetchId(_) => write!(f, "Action::FetchId"), - Self::ChangeIcon(_icon) => { - write!(f, "Action::ChangeIcon(icon)") - } - Self::Screenshot(_) => write!(f, "Action::Screenshot"), + Self::Spawn(id, settings) => { + write!(f, "Action::Spawn({id:?}, {settings:?})") + } + Self::Close(id) => write!(f, "Action::Close({id:?})"), + Self::Drag(id) => write!(f, "Action::Drag({id:?})"), + Self::Resize(id, size) => { + write!(f, "Action::Resize({id:?}, {size:?})") + } + Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), + Self::FetchMaximized(id, _) => { + write!(f, "Action::FetchMaximized({id:?})") + } + Self::Maximize(id, maximized) => { + write!(f, "Action::Maximize({id:?}, {maximized})") + } + Self::FetchMinimized(id, _) => { + write!(f, "Action::FetchMinimized({id:?})") + } + Self::Minimize(id, minimized) => { + write!(f, "Action::Minimize({id:?}, {minimized}") + } + Self::Move(id, position) => { + write!(f, "Action::Move({id:?}, {position})") + } + Self::ChangeMode(id, mode) => { + write!(f, "Action::SetMode({id:?}, {mode:?})") + } + Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), + Self::ToggleMaximize(id) => { + write!(f, "Action::ToggleMaximize({id:?})") + } + Self::ToggleDecorations(id) => { + write!(f, "Action::ToggleDecorations({id:?})") + } + Self::RequestUserAttention(id, _) => { + write!(f, "Action::RequestUserAttention({id:?})") + } + Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), + Self::ChangeLevel(id, level) => { + write!(f, "Action::ChangeLevel({id:?}, {level:?})") + } + Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), + Self::ChangeIcon(id, _icon) => { + write!(f, "Action::ChangeIcon({id:?})") + } + Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), } } } @@ -168,6 +168,9 @@ use iced_winit::runtime; pub use iced_futures::futures; +#[cfg(feature = "highlighter")] +pub use iced_highlighter as highlighter; + mod error; mod sandbox; @@ -179,6 +182,9 @@ pub mod window; #[cfg(feature = "advanced")] pub mod advanced; +#[cfg(feature = "multi-window")] +pub mod multi_window; + pub use style::theme; pub use crate::core::alignment; @@ -187,7 +193,6 @@ pub use crate::core::{ color, Alignment, Background, BorderRadius, Color, ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; -pub use crate::runtime::Command; pub mod clipboard { //! Access the clipboard. @@ -225,7 +230,8 @@ pub mod event { pub mod keyboard { //! Listen and react to keyboard events. - pub use crate::core::keyboard::{Event, KeyCode, Modifiers}; + pub use crate::core::keyboard::key; + pub use crate::core::keyboard::{Event, Key, Location, Modifiers}; pub use iced_futures::keyboard::{on_key_press, on_key_release}; } @@ -236,6 +242,11 @@ pub mod mouse { }; } +pub mod command { + //! Run asynchronous actions. + pub use crate::runtime::command::{channel, Command}; +} + pub mod subscription { //! Listen to external events in your application. pub use iced_futures::subscription::{ @@ -284,6 +295,7 @@ pub mod widget { } pub use application::Application; +pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; diff --git a/src/multi_window.rs b/src/multi_window.rs new file mode 100644 index 00000000..5b7a00b4 --- /dev/null +++ b/src/multi_window.rs @@ -0,0 +1,4 @@ +//! Leverage multi-window support in your application. +mod application; + +pub use application::Application; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs new file mode 100644 index 00000000..4a91bdf4 --- /dev/null +++ b/src/multi_window/application.rs @@ -0,0 +1,245 @@ +use crate::style::application::StyleSheet; +use crate::window; +use crate::{Command, Element, Executor, Settings, Subscription}; + +/// An interactive cross-platform multi-window application. +/// +/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`](#method.run). +/// +/// - On native platforms, it will run in its own windows. +/// - On the web, it will take control of the `<title>` and the `<body>` of the +/// document and display only the contents of the `window::Id::MAIN` window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. If you do not intend to perform any +/// background work in your program, the [`Sandbox`] trait offers a simplified +/// interface. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +/// +/// # Examples +/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. +/// +/// ## A simple "Hello, world!" +/// +/// If you just want to get started, here is a simple [`Application`] that +/// says "Hello, world!": +/// +/// ```no_run +/// use iced::{executor, window}; +/// use iced::{Command, Element, Settings, Theme}; +/// use iced::multi_window::{self, Application}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl multi_window::Application for Hello { +/// type Executor = executor::Default; +/// type Flags = (); +/// type Message = (); +/// type Theme = Theme; +/// +/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { +/// (Hello, Command::none()) +/// } +/// +/// fn title(&self, _window: window::Id) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { +/// Command::none() +/// } +/// +/// fn view(&self, _window: window::Id) -> Element<Self::Message> { +/// "Hello, world!".into() +/// } +/// } +/// ``` +/// +/// [`Sandbox`]: crate::Sandbox +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default + StyleSheet; + + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + + /// Returns the current title of the `window` of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your window when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the `window` of the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, crate::Renderer<Self::Theme>>; + + /// Returns the current [`Theme`] of the `window` of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + #[allow(unused_variables)] + fn theme(&self, window: window::Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current `Style` of the [`Theme`]. + /// + /// [`Theme`]: Self::Theme + fn style(&self) -> <Self::Theme as StyleSheet>::Style { + <Self::Theme as StyleSheet>::Style::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Returns the scale factor of the `window` of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } + + /// Runs the multi-window [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings<Self::Flags>) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::multi_window::run::< + Instance<Self>, + Self::Executor, + crate::renderer::Compositor<Self::Theme>, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance<A: Application>(A); + +impl<A> crate::runtime::multi_window::Program for Instance<A> +where + A: Application, +{ + type Renderer = crate::Renderer<A::Theme>; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) + } + + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view(window) + } +} + +impl<A> crate::shell::multi_window::Application for Instance<A> +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: window::Id) -> String { + self.0.title(window) + } + + fn theme(&self, window: window::Id) -> A::Theme { + self.0.theme(window) + } + + fn style(&self) -> <A::Theme as StyleSheet>::Style { + self.0.style() + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/settings.rs b/src/settings.rs index c5e28e86..d9476b61 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,8 @@ use crate::window; use crate::{Font, Pixels}; +use std::borrow::Cow; + /// The settings of an application. #[derive(Debug, Clone)] pub struct Settings<Flags> { @@ -21,6 +23,9 @@ pub struct Settings<Flags> { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec<Cow<'static, [u8]>>, + /// The default [`Font`] to be used. /// /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). @@ -41,14 +46,6 @@ 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. - /// - /// [`Application`]: crate::Application - pub exit_on_close_request: bool, } impl<Flags> Settings<Flags> { @@ -62,10 +59,10 @@ impl<Flags> Settings<Flags> { flags, id: default_settings.id, window: default_settings.window, + fonts: default_settings.fonts, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, - exit_on_close_request: default_settings.exit_on_close_request, } } } @@ -79,10 +76,10 @@ where id: None, window: window::Settings::default(), flags: Default::default(), + fonts: Vec::new(), default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: false, - exit_on_close_request: true, } } } @@ -91,9 +88,9 @@ 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(), + window: settings.window, flags: settings.flags, - exit_on_close_request: settings.exit_on_close_request, + fonts: settings.fonts, } } } diff --git a/src/time.rs b/src/time.rs index 37d454ed..f10f7a5e 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,4 +1,5 @@ //! Listen and react to time. pub use iced_core::time::{Duration, Instant}; +#[allow(unused_imports)] pub use iced_futures::backend::default::time::*; diff --git a/src/window.rs b/src/window.rs index e4601575..9f96da52 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,8 @@ //! Configure the window of your application in native platforms. -mod position; -mod settings; pub mod icon; pub use icon::Icon; -pub use position::Position; -pub use settings::{PlatformSpecific, Settings}; pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/src/window/position.rs b/src/window/position.rs deleted file mode 100644 index 6b9fac41..00000000 --- a/src/window/position.rs +++ /dev/null @@ -1,32 +0,0 @@ -/// 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 - } -} - -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/style/src/container.rs b/style/src/container.rs index ec543ae4..490a9dab 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -1,5 +1,5 @@ //! Change the appearance of a container. -use iced_core::{Background, BorderRadius, Color}; +use crate::core::{Background, BorderRadius, Color, Pixels}; /// The appearance of a container. #[derive(Debug, Clone, Copy)] @@ -16,6 +16,30 @@ pub struct Appearance { pub border_color: Color, } +impl Appearance { + /// Derives a new [`Appearance`] with a border of the given [`Color`] and + /// `width`. + pub fn with_border( + self, + color: impl Into<Color>, + width: impl Into<Pixels>, + ) -> Self { + Self { + border_color: color.into(), + border_width: width.into().0, + ..self + } + } + + /// Derives a new [`Appearance`] with the given [`Background`]. + pub fn with_background(self, background: impl Into<Background>) -> Self { + Self { + background: Some(background.into()), + ..self + } + } +} + impl std::default::Default for Appearance { fn default() -> Self { Self { diff --git a/style/src/lib.rs b/style/src/lib.rs index 30f17a44..e4097434 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -29,6 +29,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod svg; +pub mod text_editor; pub mod text_input; pub mod theme; pub mod toggler; diff --git a/style/src/svg.rs b/style/src/svg.rs index 9378c1a7..5053f9f8 100644 --- a/style/src/svg.rs +++ b/style/src/svg.rs @@ -20,4 +20,7 @@ pub trait StyleSheet { /// Produces the [`Appearance`] of the svg. fn appearance(&self, style: &Self::Style) -> Appearance; + + /// Produces the hovered [`Appearance`] of a svg content. + fn hovered(&self, style: &Self::Style) -> Appearance; } diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs new file mode 100644 index 00000000..f6bae7e6 --- /dev/null +++ b/style/src/text_editor.rs @@ -0,0 +1,47 @@ +//! Change the appearance of a text editor. +use crate::core::{Background, BorderRadius, Color}; + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The border radius of the text input. + pub border_radius: BorderRadius, + /// The border width of the text input. + pub border_width: f32, + /// The border [`Color`] of the text input. + pub border_color: Color, +} + +/// A set of rules that dictate the style of a text input. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the style of an active text input. + fn active(&self, style: &Self::Style) -> Appearance; + + /// Produces the style of a focused text input. + fn focused(&self, style: &Self::Style) -> Appearance; + + /// Produces the [`Color`] of the placeholder of a text input. + fn placeholder_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a text input. + fn value_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a disabled text input. + fn disabled_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the selection of a text input. + fn selection_color(&self, style: &Self::Style) -> Color; + + /// Produces the style of an hovered text input. + fn hovered(&self, style: &Self::Style) -> Appearance { + self.focused(style) + } + + /// Produces the style of a disabled text input. + fn disabled(&self, style: &Self::Style) -> Appearance; +} diff --git a/style/src/theme.rs b/style/src/theme.rs index 3c1f2de6..f78587e5 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -17,11 +17,13 @@ use crate::rule; use crate::scrollable; use crate::slider; use crate::svg; +use crate::text_editor; use crate::text_input; use crate::toggler; use iced_core::{Background, Color, Vector}; +use std::fmt; use std::rc::Rc; /// A built-in theme. @@ -37,18 +39,22 @@ pub enum Theme { } impl Theme { + /// A list with all the defined themes. + pub const ALL: &'static [Self] = &[Self::Light, Self::Dark]; + /// Creates a new custom [`Theme`] from the given [`Palette`]. - pub fn custom(palette: Palette) -> Self { - Self::custom_with_fn(palette, palette::Extended::generate) + pub fn custom(name: String, palette: Palette) -> Self { + Self::custom_with_fn(name, palette, palette::Extended::generate) } /// Creates a new custom [`Theme`] from the given [`Palette`], with /// a custom generator of a [`palette::Extended`]. pub fn custom_with_fn( + name: String, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { - Self::Custom(Box::new(Custom::with_fn(palette, generate))) + Self::Custom(Box::new(Custom::with_fn(name, palette, generate))) } /// Returns the [`Palette`] of the [`Theme`]. @@ -70,32 +76,51 @@ impl Theme { } } +impl fmt::Display for Theme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Light => write!(f, "Light"), + Self::Dark => write!(f, "Dark"), + Self::Custom(custom) => custom.fmt(f), + } + } +} + /// A [`Theme`] with a customized [`Palette`]. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Custom { + name: String, palette: Palette, extended: palette::Extended, } impl Custom { /// Creates a [`Custom`] theme from the given [`Palette`]. - pub fn new(palette: Palette) -> Self { - Self::with_fn(palette, palette::Extended::generate) + pub fn new(name: String, palette: Palette) -> Self { + Self::with_fn(name, palette, palette::Extended::generate) } /// Creates a [`Custom`] theme from the given [`Palette`] with /// a custom generator of a [`palette::Extended`]. pub fn with_fn( + name: String, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { Self { + name, palette, extended: generate(palette), } } } +impl fmt::Display for Custom { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + /// The style of an application. #[derive(Default)] pub enum Application { @@ -382,6 +407,12 @@ pub enum Container { Custom(Box<dyn container::StyleSheet<Style = Theme>>), } +impl From<container::Appearance> for Container { + fn from(appearance: container::Appearance) -> Self { + Self::Custom(Box::new(move |_: &_| appearance)) + } +} + impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container { fn from(f: T) -> Self { Self::Custom(Box::new(f)) @@ -908,6 +939,10 @@ impl svg::StyleSheet for Theme { Svg::Custom(custom) => custom.appearance(self), } } + + fn hovered(&self, style: &Self::Style) -> svg::Appearance { + self.appearance(style) + } } impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { @@ -916,6 +951,10 @@ impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { fn appearance(&self, style: &Self::Style) -> svg::Appearance { (self)(style) } + + fn hovered(&self, style: &Self::Style) -> svg::Appearance { + self.appearance(style) + } } /// The style of a scrollable. @@ -1174,3 +1213,115 @@ impl text_input::StyleSheet for Theme { self.placeholder_color(style) } } + +/// The style of a text input. +#[derive(Default)] +pub enum TextEditor { + /// The default style. + #[default] + Default, + /// A custom style. + Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>), +} + +impl text_editor::StyleSheet for Theme { + type Style = TextEditor; + + fn active(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.active(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.hovered(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.focused(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.placeholder_color(self); + } + + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.value_color(self); + } + + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.selection_color(self); + } + + let palette = self.extended_palette(); + + palette.primary.weak.color + } + + fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.disabled(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.weak.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn disabled_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.disabled_color(self); + } + + self.placeholder_color(style) + } +} diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index aaeb799d..76977a29 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -82,6 +82,8 @@ pub struct Extended { pub success: Success, /// The set of danger colors. pub danger: Danger, + /// Whether the palette is dark or not. + pub is_dark: bool, } /// The built-in light variant of an [`Extended`] palette. @@ -113,6 +115,7 @@ impl Extended { palette.background, palette.text, ), + is_dark: is_dark(palette.background), } } } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 15a6928a..68b2a03a 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -22,15 +22,10 @@ bytemuck.workspace = true cosmic-text.workspace = true kurbo.workspace = true log.workspace = true -raw-window-handle.workspace = true rustc-hash.workspace = true softbuffer.workspace = true tiny-skia.workspace = true -twox-hash.workspace = true +xxhash-rust.workspace = true resvg.workspace = true resvg.optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -twox-hash.workspace = true -twox-hash.features = ["std"] diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 65aca4b0..d1393b4d 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,7 +1,7 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; use crate::graphics::text; -use crate::graphics::{Damage, Viewport}; +use crate::graphics::Viewport; use crate::primitive::{self, Primitive}; use std::borrow::Cow; @@ -362,11 +362,10 @@ impl Backend { paragraph, position, color, + clip_bounds: text_clip_bounds, } => { let physical_bounds = - (Rectangle::new(*position, paragraph.min_bounds) - + translation) - * scale_factor; + (*text_clip_bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -384,6 +383,31 @@ impl Backend { clip_mask, ); } + Primitive::Editor { + editor, + position, + color, + clip_bounds: text_clip_bounds, + } => { + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_editor( + editor, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } Primitive::Text { content, bounds, @@ -394,9 +418,10 @@ impl Backend { horizontal_alignment, vertical_alignment, shaping, + clip_bounds: text_clip_bounds, } => { let physical_bounds = - (primitive.bounds() + translation) * scale_factor; + (*text_clip_bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -420,8 +445,41 @@ impl Backend { clip_mask, ); } + Primitive::RawText(text::Raw { + buffer, + position, + color, + clip_bounds: text_clip_bounds, + }) => { + let Some(buffer) = buffer.upgrade() else { + return; + }; + + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } #[cfg(feature = "image")] - Primitive::Image { handle, bounds } => { + Primitive::Image { + handle, + filter_method, + bounds, + } => { let physical_bounds = (*bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { @@ -437,8 +495,14 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - self.raster_pipeline - .draw(handle, *bounds, pixels, transform, clip_mask); + self.raster_pipeline.draw( + handle, + *filter_method, + *bounds, + pixels, + transform, + clip_mask, + ); } #[cfg(not(feature = "image"))] Primitive::Image { .. } => { @@ -479,7 +543,6 @@ impl Backend { path, paint, rule, - transform, }) => { let bounds = path.bounds(); @@ -502,9 +565,11 @@ impl Backend { path, paint, *rule, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } @@ -512,7 +577,6 @@ impl Backend { path, paint, stroke, - transform, }) => { let bounds = path.bounds(); @@ -535,9 +599,11 @@ impl Backend { path, paint, stroke, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } @@ -803,10 +869,6 @@ impl iced_graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 1d14aa03..74a08d38 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,4 +1,5 @@ -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::text::LineHeight; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; @@ -39,17 +40,22 @@ impl Frame { } pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); + let mut paint = into_paint(fill.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: into_paint(fill.style), + paint, rule: into_fill_rule(fill.rule), - transform: self.transform, })); } @@ -59,73 +65,111 @@ impl Frame { size: Size, fill: impl Into<Fill>, ) { - let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { + let Some(path) = convert_path(&Path::rectangle(top_left, size)) + .and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); + let mut paint = tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }; + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, + paint, rule: into_fill_rule(fill.rule), - transform: self.transform, })); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; let stroke = stroke.into(); let skia_stroke = into_stroke(&stroke); + let mut paint = into_paint(stroke.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Stroke { path, - paint: into_paint(stroke.style), + paint, stroke: skia_stroke, - transform: self.transform, })); } pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let position = if self.transform.is_identity() { - text.position - } else { - let mut transformed = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut transformed); - - Point::new(transformed[0].x, transformed[0].y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { + let (scale_x, scale_y) = self.transform.get_scale(); + + if self.transform.is_scale_translate() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = if self.transform.is_identity() + { + (text.position, text.size, text.line_height) + } else { + let mut position = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut position); + + let size = text.size.0 * scale_y; + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + ( + Point::new(position[0].x, position[0].y), + size.into(), + line_height, + ) + }; + + let bounds = Rectangle { x: position.x, y: position.y, width: f32::INFINITY, height: f32::INFINITY, - }, - color: text.color, - size: text.size, - line_height: text.line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - }); + }; + + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, + line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } } pub fn push_transform(&mut self) { diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 0ed24969..7718d542 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -13,8 +13,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The fill rule to follow. rule: tiny_skia::FillRule, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, /// A path stroked with some paint. Stroke { @@ -24,8 +22,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The stroke settings. stroke: tiny_skia::Stroke, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index d13b1167..5f17ae60 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -28,6 +28,7 @@ impl Pipeline { pub fn draw( &mut self, handle: &raster::Handle, + filter_method: raster::FilterMethod, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, @@ -39,12 +40,21 @@ impl Pipeline { let transform = transform.pre_scale(width_scale, height_scale); + let quality = match filter_method { + raster::FilterMethod::Linear => { + tiny_skia::FilterQuality::Bilinear + } + raster::FilterMethod::Nearest => { + tiny_skia::FilterQuality::Nearest + } + }; + pixels.draw_pixmap( (bounds.x / width_scale) as i32, (bounds.y / height_scale) as i32, image, &tiny_skia::PixmapPaint { - quality: tiny_skia::FilterQuality::Bilinear, + quality, ..Default::default() }, transform, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index cb3ef54c..9413e311 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,9 +1,10 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Size}; use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::editor; +use crate::graphics::text::font_system; use crate::graphics::text::paragraph; -use crate::graphics::text::FontSystem; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; @@ -12,7 +13,6 @@ use std::collections::hash_map; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, glyph_cache: GlyphCache, cache: RefCell<Cache>, } @@ -20,18 +20,16 @@ pub struct Pipeline { impl Pipeline { pub fn new() -> Self { Pipeline { - font_system: FontSystem::new(), glyph_cache: GlyphCache::new(), cache: RefCell::new(Cache::new()), } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -51,8 +49,10 @@ impl Pipeline { return; }; + let mut font_system = font_system().write().expect("Write font system"); + draw( - self.font_system.get_mut(), + font_system.raw(), &mut self.glyph_cache, paragraph.buffer(), Rectangle::new(position, paragraph.min_bounds()), @@ -65,6 +65,37 @@ impl Pipeline { ); } + pub fn draw_editor( + &mut self, + editor: &editor::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Editor as _; + + let Some(editor) = editor.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + editor.buffer(), + Rectangle::new(position, editor.bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn draw_cached( &mut self, content: &str, @@ -82,7 +113,9 @@ impl Pipeline { ) { let line_height = f32::from(line_height.to_absolute(size)); - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let key = cache::Key { bounds: bounds.size(), content, @@ -115,6 +148,33 @@ impl Pipeline { ); } + pub fn draw_raw( + &mut self, + buffer: &cosmic_text::Buffer, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + let mut font_system = font_system().write().expect("Write font system"); + + let (width, height) = buffer.size(); + + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + Rectangle::new(position, Size::new(width, height)), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn trim_cache(&mut self) { self.cache.get_mut().trim(); self.glyph_cache.trim(); @@ -155,7 +215,7 @@ fn draw( if let Some((buffer, placement)) = glyph_cache.allocate( physical_glyph.cache_key, - color, + glyph.color_opt.map(from_color).unwrap_or(color), font_system, &mut swash, ) { @@ -180,6 +240,12 @@ fn draw( } } +fn from_color(color: cosmic_text::Color) -> Color { + let [r, g, b, a] = color.as_rgba(); + + Color::from_rgba8(r, g, b, a as f32 / 255.0) +} + #[derive(Debug, Clone, Default)] struct GlyphCache { entries: FxHashMap< diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index a1cd269d..fd1ab3de 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,7 +1,8 @@ use crate::core::svg::{Data, Handle}; use crate::core::{Color, Rectangle, Size}; +use crate::graphics::text; -use resvg::usvg; +use resvg::usvg::{self, TreeTextToPath}; use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; @@ -77,7 +78,7 @@ impl Cache { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let svg = match handle.data() { + let mut svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { usvg::Tree::from_str( @@ -92,6 +93,15 @@ impl Cache { } }; + if let Some(svg) = &mut svg { + if svg.has_text_nodes() { + let mut font_system = + text::font_system().write().expect("Write font system"); + + svg.convert_text(font_system.raw().db_mut()); + } + } + let _ = entry.insert(svg); } diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 828e522f..781ed8a5 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -4,19 +4,25 @@ use crate::graphics::damage; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::collections::VecDeque; use std::marker::PhantomData; +use std::num::NonZeroU32; pub struct Compositor<Theme> { + context: softbuffer::Context<Box<dyn compositor::Window>>, + settings: Settings, _theme: PhantomData<Theme>, } pub struct Surface { - window: softbuffer::GraphicsContext, - buffer: Vec<u32>, + window: softbuffer::Surface< + Box<dyn compositor::Window>, + Box<dyn compositor::Window>, + >, clip_mask: tiny_skia::Mask, - primitives: Option<Vec<Primitive>>, + primitive_stack: VecDeque<Vec<Primitive>>, background_color: Color, + max_age: u8, } impl<Theme> crate::graphics::Compositor for Compositor<Theme> { @@ -24,53 +30,64 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { type Renderer = Renderer<Theme>; type Surface = Surface; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new<W: compositor::Window>( settings: Self::Settings, - _compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(); + compatible_window: W, + ) -> Result<Self, Error> { + Ok(new(settings, compatible_window)) + } - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + fn create_renderer(&self) -> Self::Renderer { + Renderer::new( + Backend::new(), + self.settings.default_font, + self.settings.default_text_size, + ) } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface<W: compositor::Window + Clone>( &mut self, - window: &W, + window: W, width: u32, height: u32, - ) -> Surface { - #[allow(unsafe_code)] - let window = - unsafe { softbuffer::GraphicsContext::new(window, window) } - .expect("Create softbuffer for window"); + ) -> Self::Surface { + let window = softbuffer::Surface::new( + &self.context, + Box::new(window.clone()) as _, + ) + .expect("Create softbuffer surface for window"); - Surface { + let mut surface = Surface { window, - buffer: vec![0; width as usize * height as usize], clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), - primitives: None, + primitive_stack: VecDeque::new(), background_color: Color::BLACK, - } + max_age: 0, + }; + + self.configure_surface(&mut surface, width, height); + + surface } fn configure_surface( &mut self, - surface: &mut Surface, + surface: &mut Self::Surface, width: u32, height: u32, ) { - surface.buffer.resize((width * height) as usize, 0); + surface + .window + .resize( + NonZeroU32::new(width).expect("Non-zero width"), + NonZeroU32::new(height).expect("Non-zero height"), + ) + .expect("Resize surface"); + surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); - surface.primitives = None; + surface.primitive_stack.clear(); } fn fetch_information(&self) -> Information { @@ -121,13 +138,19 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } -pub fn new<Theme>() -> (Compositor<Theme>, Backend) { - ( - Compositor { - _theme: PhantomData, - }, - Backend::new(), - ) +pub fn new<W: compositor::Window, Theme>( + settings: Settings, + compatible_window: W, +) -> Compositor<Theme> { + #[allow(unsafe_code)] + let context = softbuffer::Context::new(Box::new(compatible_window) as _) + .expect("Create softbuffer context"); + + Compositor { + context, + settings, + _theme: PhantomData, + } } pub fn present<T: AsRef<str>>( @@ -141,16 +164,25 @@ pub fn present<T: AsRef<str>>( let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; - let mut pixels = tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut surface.buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"); + let mut buffer = surface + .window + .buffer_mut() + .map_err(|_| compositor::SurfaceError::Lost)?; + + let last_primitives = { + let age = buffer.age(); - let damage = surface - .primitives - .as_deref() + surface.max_age = surface.max_age.max(age); + surface.primitive_stack.truncate(surface.max_age as usize); + + if age > 0 { + surface.primitive_stack.get(age as usize - 1) + } else { + None + } + }; + + let damage = last_primitives .and_then(|last_primitives| { (surface.background_color == background_color) .then(|| damage::list(last_primitives, primitives)) @@ -161,11 +193,18 @@ pub fn present<T: AsRef<str>>( return Ok(()); } - surface.primitives = Some(primitives.to_vec()); + surface.primitive_stack.push_front(primitives.to_vec()); surface.background_color = background_color; let damage = damage::group(damage, scale_factor, physical_size); + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + backend.draw( &mut pixels, &mut surface.clip_mask, @@ -176,13 +215,7 @@ pub fn present<T: AsRef<str>>( overlay, ); - surface.window.set_buffer( - &surface.buffer, - physical_size.width as u16, - physical_size.height as u16, - ); - - Ok(()) + buffer.present().map_err(|_| compositor::SurfaceError::Lost) } pub fn screenshot<T: AsRef<str>>( diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index a460c127..1d3b57a7 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -32,7 +32,6 @@ glyphon.workspace = true guillotiere.workspace = true log.workspace = true once_cell.workspace = true -raw-window-handle.workspace = true wgpu.workspace = true lyon.workspace = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 65c63f19..25134d68 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,8 +1,8 @@ use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::backend; use crate::graphics::color; use crate::graphics::{Transformation, Viewport}; +use crate::primitive::pipeline; use crate::primitive::{self, Primitive}; use crate::quad; use crate::text; @@ -26,6 +26,7 @@ pub struct Backend { quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, + pipeline_storage: pipeline::Storage, #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, @@ -51,6 +52,7 @@ impl Backend { quad_pipeline, text_pipeline, triangle_pipeline, + pipeline_storage: pipeline::Storage::default(), #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, @@ -67,6 +69,7 @@ impl Backend { queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, clear_color: Option<Color>, + format: wgpu::TextureFormat, frame: &wgpu::TextureView, primitives: &[Primitive], viewport: &Viewport, @@ -89,6 +92,7 @@ impl Backend { self.prepare( device, queue, + format, encoder, scale_factor, target_size, @@ -118,6 +122,7 @@ impl Backend { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + format: wgpu::TextureFormat, _encoder: &mut wgpu::CommandEncoder, scale_factor: f32, target_size: Size<u32>, @@ -180,6 +185,20 @@ impl Backend { target_size, ); } + + if !layer.pipelines.is_empty() { + for pipeline in &layer.pipelines { + pipeline.primitive.prepare( + format, + device, + queue, + pipeline.bounds, + target_size, + scale_factor, + &mut self.pipeline_storage, + ); + } + } } } @@ -203,7 +222,7 @@ impl Backend { let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), + label: Some("iced_wgpu render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, @@ -222,10 +241,12 @@ impl Backend { }), None => wgpu::LoadOp::Load, }, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }, )); @@ -264,18 +285,20 @@ impl Backend { render_pass = ManuallyDrop::new(encoder.begin_render_pass( &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), + label: Some("iced_wgpu render pass"), color_attachments: &[Some( wgpu::RenderPassColorAttachment { view: target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, }, )], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }, )); } @@ -299,6 +322,45 @@ impl Backend { text_layer += 1; } + + if !layer.pipelines.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + for pipeline in &layer.pipelines { + let viewport = (pipeline.viewport * scale_factor).snap(); + + if viewport.width < 1 || viewport.height < 1 { + continue; + } + + pipeline.primitive.render( + &self.pipeline_storage, + target, + target_size, + viewport, + encoder, + ); + } + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } } let _ = ManuallyDrop::into_inner(render_pass); @@ -310,10 +372,6 @@ impl crate::graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &graphics::text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 20827e3c..4598b0a6 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -143,10 +143,12 @@ pub fn convert( resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); pass.set_pipeline(&pipeline); diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 655362b7..4d7f443e 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,5 +1,6 @@ //! Build and draw geometry. -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::text::LineHeight; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -115,19 +116,31 @@ struct Transforms { } #[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} +struct Transform(lyon::math::Transform); impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { + fn is_identity(&self) -> bool { + self.0 == lyon::math::Transform::identity() + } + + fn is_scale_translation(&self) -> bool { + self.0.m12.abs() < 2.0 * f32::EPSILON + && self.0.m21.abs() < 2.0 * f32::EPSILON + } + + fn scale(&self) -> (f32, f32) { + (self.0.m11, self.0.m22) + } + + fn transform_point(&self, point: Point) -> Point { let transformed = self - .raw + .0 .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; + + Point { + x: transformed.x, + y: transformed.y, + } } fn transform_style(&self, style: Style) -> Style { @@ -142,8 +155,8 @@ impl Transform { fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { match &mut gradient { Gradient::Linear(linear) => { - self.transform_point(&mut linear.start); - self.transform_point(&mut linear.end); + linear.start = self.transform_point(linear.start); + linear.end = self.transform_point(linear.end); } } @@ -163,10 +176,7 @@ impl Frame { primitives: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, + current: Transform(lyon::math::Transform::identity()), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), @@ -209,14 +219,14 @@ impl Frame { let options = tessellation::FillOptions::default() .with_fill_rule(into_fill_rule(rule)); - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.fill_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.fill_tessellator.tessellate_path( path.raw(), @@ -241,13 +251,14 @@ impl Frame { .buffers .get_fill(&self.transforms.current.transform_style(style)); - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); let size = - self.transforms.current.raw.transform_vector( + self.transforms.current.0.transform_vector( lyon::math::Vector::new(size.width, size.height), ); @@ -284,14 +295,14 @@ impl Frame { Cow::Owned(dashed(path, stroke.line_dash)) }; - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.stroke_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.stroke_tessellator.tessellate_path( path.raw(), @@ -318,33 +329,57 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); + let (scale_x, scale_y) = self.transforms.current.scale(); + + if self.transforms.current.is_scale_translation() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = + if self.transforms.current.is_identity() { + (text.position, text.size, text.line_height) + } else { + let position = + self.transforms.current.transform_point(text.position); + + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; - Point::new(transformed.x, transformed.y) - }; + (position, size, line_height) + }; - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { + let bounds = Rectangle { x: position.x, y: position.y, width: f32::INFINITY, height: f32::INFINITY, - }, - color: text.color, - size: text.size, - line_height: text.line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - }); + }; + + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, + line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } } /// Stores the current transform of the [`Frame`] and executes the given @@ -420,26 +455,24 @@ impl Frame { /// Applies a translation to the current transform of the [`Frame`]. #[inline] pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); } /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self + self.transforms.current.0 = self .transforms .current - .raw + .0 .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; } /// Applies a uniform scaling to the current transform of the [`Frame`]. @@ -455,9 +488,8 @@ impl Frame { pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { let scale = scale.into(); - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale.x, scale.y); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); } /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 36c1e228..1e5d3ee0 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -35,7 +35,8 @@ pub struct Pipeline { vector_cache: RefCell<vector::Cache>, pipeline: wgpu::RenderPipeline, - sampler: wgpu::Sampler, + nearest_sampler: wgpu::Sampler, + linear_sampler: wgpu::Sampler, texture: wgpu::BindGroup, texture_version: usize, texture_atlas: Atlas, @@ -49,16 +50,16 @@ pub struct Pipeline { #[derive(Debug)] struct Layer { uniforms: wgpu::Buffer, - constants: wgpu::BindGroup, - instances: Buffer<Instance>, - instance_count: usize, + nearest: Data, + linear: Data, } impl Layer { fn new( device: &wgpu::Device, constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, ) -> Self { let uniforms = device.create_buffer(&wgpu::BufferDescriptor { label: Some("iced_wgpu::image uniforms buffer"), @@ -67,6 +68,59 @@ impl Layer { mapped_at_creation: false, }); + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + queue.write_buffer( + &self.uniforms, + 0, + bytemuck::bytes_of(&Uniforms { + transform: transformation.into(), + }), + ); + + self.nearest.upload(device, queue, nearest_instances); + self.linear.upload(device, queue, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer<Instance>, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image constants bind group"), layout: constant_layout, @@ -75,7 +129,7 @@ impl Layer { binding: 0, resource: wgpu::BindingResource::Buffer( wgpu::BufferBinding { - buffer: &uniforms, + buffer: uniforms, offset: 0, size: None, }, @@ -96,28 +150,18 @@ impl Layer { ); Self { - uniforms, constants, instances, instance_count: 0, } } - fn prepare( + fn upload( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, instances: &[Instance], - transformation: Transformation, ) { - queue.write_buffer( - &self.uniforms, - 0, - bytemuck::bytes_of(&Uniforms { - transform: transformation.into(), - }), - ); - let _ = self.instances.resize(device, instances.len()); let _ = self.instances.write(queue, 0, instances); @@ -134,12 +178,22 @@ impl Layer { impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Nearest, + mag_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, ..Default::default() }); @@ -286,7 +340,8 @@ impl Pipeline { vector_cache: RefCell::new(vector::Cache::default()), pipeline, - sampler, + nearest_sampler, + linear_sampler, texture, texture_version: texture_atlas.layer_count(), texture_atlas, @@ -329,7 +384,8 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); - let instances: &mut Vec<Instance> = &mut Vec::new(); + let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); + let linear_instances: &mut Vec<Instance> = &mut Vec::new(); #[cfg(feature = "image")] let mut raster_cache = self.raster_cache.borrow_mut(); @@ -340,7 +396,11 @@ impl Pipeline { for image in images { match &image { #[cfg(feature = "image")] - layer::Image::Raster { handle, bounds } => { + layer::Image::Raster { + handle, + filter_method, + bounds, + } => { if let Some(atlas_entry) = raster_cache.upload( device, encoder, @@ -351,7 +411,12 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, - instances, + match filter_method { + image::FilterMethod::Nearest => { + nearest_instances + } + image::FilterMethod::Linear => linear_instances, + }, ); } } @@ -379,7 +444,7 @@ impl Pipeline { [bounds.x, bounds.y], size, atlas_entry, - instances, + nearest_instances, ); } } @@ -388,7 +453,7 @@ impl Pipeline { } } - if instances.is_empty() { + if nearest_instances.is_empty() && linear_instances.is_empty() { return; } @@ -416,12 +481,20 @@ impl Pipeline { self.layers.push(Layer::new( device, &self.constant_layout, - &self.sampler, + &self.nearest_sampler, + &self.linear_sampler, )); } let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, instances, transformation); + + layer.prepare( + device, + queue, + nearest_instances, + linear_instances, + transformation, + ); self.prepare_layer += 1; } @@ -470,7 +543,7 @@ struct Instance { } impl Instance { - pub const INITIAL: usize = 1_000; + pub const INITIAL: usize = 20; } #[repr(C)] diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 6582bb82..d9be50d7 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,9 +1,10 @@ use crate::core::svg; use crate::core::{Color, Size}; +use crate::graphics::text; use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; -use resvg::usvg; +use resvg::usvg::{self, TreeTextToPath}; use std::collections::{HashMap, HashSet}; use std::fs; @@ -49,15 +50,15 @@ impl Cache { return self.svgs.get(&handle.id()).unwrap(); } - let svg = match handle.data() { - svg::Data::Path(path) => { - let tree = fs::read_to_string(path).ok().and_then(|contents| { + let mut svg = match handle.data() { + svg::Data::Path(path) => fs::read_to_string(path) + .ok() + .and_then(|contents| { usvg::Tree::from_str(&contents, &usvg::Options::default()) .ok() - }); - - tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) - } + }) + .map(Svg::Loaded) + .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { match usvg::Tree::from_data(bytes, &usvg::Options::default()) { Ok(tree) => Svg::Loaded(tree), @@ -66,6 +67,15 @@ impl Cache { } }; + if let Svg::Loaded(svg) = &mut svg { + if svg.has_text_nodes() { + let mut font_system = + text::font_system().write().expect("Write font system"); + + svg.convert_text(font_system.raw().db_mut()); + } + } + let _ = self.svgs.insert(handle.id(), svg); self.svgs.get(&handle.id()).unwrap() } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index d20dbe66..4ad12a88 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,11 +1,13 @@ //! Organize rendering primitives into a flattened list of layers. mod image; +mod pipeline; mod text; pub mod mesh; pub use image::Image; pub use mesh::Mesh; +pub use pipeline::Pipeline; pub use text::Text; use crate::core; @@ -34,6 +36,9 @@ pub struct Layer<'a> { /// The images of the [`Layer`]. pub images: Vec<Image>, + + /// The custom pipelines of this [`Layer`]. + pub pipelines: Vec<Pipeline>, } impl<'a> Layer<'a> { @@ -45,6 +50,7 @@ impl<'a> Layer<'a> { meshes: Vec::new(), text: Vec::new(), images: Vec::new(), + pipelines: Vec::new(), } } @@ -69,6 +75,7 @@ impl<'a> Layer<'a> { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, + clip_bounds: Rectangle::with_size(Size::INFINITY), }; overlay.text.push(Text::Cached(text.clone())); @@ -117,13 +124,30 @@ impl<'a> Layer<'a> { paragraph, position, color, + clip_bounds, } => { let layer = &mut layers[current_layer]; - layer.text.push(Text::Managed { + layer.text.push(Text::Paragraph { paragraph: paragraph.clone(), position: *position + translation, color: *color, + clip_bounds: *clip_bounds + translation, + }); + } + Primitive::Editor { + editor, + position, + color, + clip_bounds, + } => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Editor { + editor: editor.clone(), + position: *position + translation, + color: *color, + clip_bounds: *clip_bounds + translation, }); } Primitive::Text { @@ -136,6 +160,7 @@ impl<'a> Layer<'a> { horizontal_alignment, vertical_alignment, shaping, + clip_bounds, } => { let layer = &mut layers[current_layer]; @@ -149,6 +174,22 @@ impl<'a> Layer<'a> { horizontal_alignment: *horizontal_alignment, vertical_alignment: *vertical_alignment, shaping: *shaping, + clip_bounds: *clip_bounds + translation, + })); + } + graphics::Primitive::RawText(graphics::text::Raw { + buffer, + position, + color, + clip_bounds, + }) => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Raw(graphics::text::Raw { + buffer: buffer.clone(), + position: *position + translation, + color: *color, + clip_bounds: *clip_bounds + translation, })); } Primitive::Quad { @@ -173,11 +214,16 @@ impl<'a> Layer<'a> { layer.quads.add(quad, background); } - Primitive::Image { handle, bounds } => { + Primitive::Image { + handle, + filter_method, + bounds, + } => { let layer = &mut layers[current_layer]; layer.images.push(Image::Raster { handle: handle.clone(), + filter_method: *filter_method, bounds: *bounds + translation, }); } @@ -290,6 +336,20 @@ impl<'a> Layer<'a> { } } }, + primitive::Custom::Pipeline(pipeline) => { + let layer = &mut layers[current_layer]; + let bounds = pipeline.bounds + translation; + + if let Some(clip_bounds) = + layer.bounds.intersection(&bounds) + { + layer.pipelines.push(Pipeline { + bounds, + viewport: clip_bounds, + primitive: pipeline.primitive.clone(), + }); + } + } }, } } diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs index 0de589f8..facbe192 100644 --- a/wgpu/src/layer/image.rs +++ b/wgpu/src/layer/image.rs @@ -10,6 +10,9 @@ pub enum Image { /// The handle of a raster image. handle: image::Handle, + /// The filter method of a raster image. + filter_method: image::FilterMethod, + /// The bounds of the image. bounds: Rectangle, }, diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs new file mode 100644 index 00000000..6dfe6750 --- /dev/null +++ b/wgpu/src/layer/pipeline.rs @@ -0,0 +1,17 @@ +use crate::core::Rectangle; +use crate::primitive::pipeline::Primitive; + +use std::sync::Arc; + +#[derive(Clone, Debug)] +/// A custom primitive which can be used to render primitives associated with a custom pipeline. +pub struct Pipeline { + /// The bounds of the [`Pipeline`]. + pub bounds: Rectangle, + + /// The viewport of the [`Pipeline`]. + pub viewport: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Arc<dyn Primitive>, +} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index b61615d6..37ee5247 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,17 +1,33 @@ use crate::core::alignment; use crate::core::text; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics; +use crate::graphics::text::editor; use crate::graphics::text::paragraph; -/// A paragraph of text. +/// A text primitive. #[derive(Debug, Clone)] pub enum Text<'a> { - Managed { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { paragraph: paragraph::Weak, position: Point, color: Color, + clip_bounds: Rectangle, }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + }, + /// Some cached text. Cached(Cached<'a>), + /// Some raw text. + Raw(graphics::text::Raw), } #[derive(Debug, Clone)] @@ -42,4 +58,7 @@ pub struct Cached<'a> { /// The shaping strategy of the text. pub shaping: text::Shaping, + + /// The clip bounds of the text. + pub clip_bounds: Rectangle, } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6d26723e..424dfeb3 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -23,7 +23,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 8dbf3008..fff927ea 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,7 +1,13 @@ //! Draw using different graphical primitives. +pub mod pipeline; + +pub use pipeline::Pipeline; + use crate::core::Rectangle; use crate::graphics::{Damage, Mesh}; +use std::fmt::Debug; + /// The graphical primitives supported by `iced_wgpu`. pub type Primitive = crate::graphics::Primitive<Custom>; @@ -10,12 +16,15 @@ pub type Primitive = crate::graphics::Primitive<Custom>; pub enum Custom { /// A mesh primitive. Mesh(Mesh), + /// A custom pipeline primitive. + Pipeline(Pipeline), } impl Damage for Custom { fn bounds(&self) -> Rectangle { match self { Self::Mesh(mesh) => mesh.bounds(), + Self::Pipeline(pipeline) => pipeline.bounds, } } } diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs new file mode 100644 index 00000000..c8e45458 --- /dev/null +++ b/wgpu/src/primitive/pipeline.rs @@ -0,0 +1,116 @@ +//! Draw primitives using custom pipelines. +use crate::core::{Rectangle, Size}; + +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; + +#[derive(Clone, Debug)] +/// A custom primitive which can be used to render primitives associated with a custom pipeline. +pub struct Pipeline { + /// The bounds of the [`Pipeline`]. + pub bounds: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Arc<dyn Primitive>, +} + +impl Pipeline { + /// Creates a new [`Pipeline`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Pipeline { + bounds, + primitive: Arc::new(primitive), + } + } +} + +impl PartialEq for Pipeline { + fn eq(&self, other: &Self) -> bool { + self.primitive.type_id() == other.primitive.type_id() + } +} + +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + bounds: Rectangle, + target_size: Size<u32>, + scale_factor: f32, + storage: &mut Storage, + ); + + /// Renders the [`Primitive`]. + fn render( + &self, + storage: &Storage, + target: &wgpu::TextureView, + target_size: Size<u32>, + viewport: Rectangle<u32>, + encoder: &mut wgpu::CommandEncoder, + ); +} + +/// A renderer than can draw custom pipeline primitives. +pub trait Renderer: crate::core::Renderer { + /// Draws a custom pipeline primitive. + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl Primitive, + ); +} + +impl<Theme> Renderer for crate::Renderer<Theme> { + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl Primitive, + ) { + self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline( + Pipeline::new(bounds, primitive), + ))); + } +} + +/// Stores custom, user-provided pipelines. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: HashMap<TypeId, Box<dyn Any + Send>>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a pipeline with type `T`. + pub fn has<T: 'static>(&self) -> bool { + self.pipelines.get(&TypeId::of::<T>()).is_some() + } + + /// Inserts the pipeline `T` in to [`Storage`]. + pub fn store<T: 'static + Send>(&mut self, pipeline: T) { + let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); + } + + /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. + pub fn get<T: 'static>(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_ref::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } + + /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. + pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_mut::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } +} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2a530cad..dca09cb8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,15 +2,15 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{FontSystem, Paragraph}; +use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use crate::layer::Text; use std::borrow::Cow; use std::cell::RefCell; +use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, prepare_layer: usize, @@ -24,7 +24,6 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { - font_system: FontSystem::new(), renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( device, @@ -41,12 +40,11 @@ impl Pipeline { } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -69,21 +67,28 @@ impl Pipeline { )); } - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let renderer = &mut self.renderers[self.prepare_layer]; let cache = self.cache.get_mut(); enum Allocation { Paragraph(Paragraph), + Editor(Editor), Cache(cache::KeyHash), + Raw(Arc<glyphon::Buffer>), } let allocations: Vec<_> = sections .iter() .map(|section| match section { - Text::Managed { paragraph, .. } => { + Text::Paragraph { paragraph, .. } => { paragraph.upgrade().map(Allocation::Paragraph) } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } Text::Cached(text) => { let (key, _) = cache.allocate( font_system, @@ -104,6 +109,7 @@ impl Pipeline { Some(Allocation::Cache(key)) } + Text::Raw(text) => text.buffer.upgrade().map(Allocation::Raw), }) .collect(); @@ -117,9 +123,13 @@ impl Pipeline { horizontal_alignment, vertical_alignment, color, + clip_bounds, ) = match section { - Text::Managed { - position, color, .. + Text::Paragraph { + position, + color, + clip_bounds, + .. } => { use crate::core::text::Paragraph as _; @@ -134,6 +144,29 @@ impl Pipeline { paragraph.horizontal_alignment(), paragraph.vertical_alignment(), *color, + *clip_bounds, + ) + } + Text::Editor { + position, + color, + clip_bounds, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation + else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, ) } Text::Cached(text) => { @@ -152,6 +185,26 @@ impl Pipeline { text.horizontal_alignment, text.vertical_alignment, text.color, + text.clip_bounds, + ) + } + Text::Raw(text) => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new( + text.position, + Size::new(width, height), + ), + alignment::Horizontal::Left, + alignment::Vertical::Top, + text.color, + text.clip_bounds, ) } }; @@ -174,13 +227,8 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - bounds.height, }; - let section_bounds = Rectangle { - x: left, - y: top, - ..bounds - }; - - let clip_bounds = layer_bounds.intersection(§ion_bounds)?; + let clip_bounds = + layer_bounds.intersection(&(clip_bounds * scale_factor))?; Some(glyphon::TextArea { buffer, @@ -193,16 +241,7 @@ impl Pipeline { right: (clip_bounds.x + clip_bounds.width) as i32, bottom: (clip_bounds.y + clip_bounds.height) as i32, }, - default_color: { - let [r, g, b, a] = color::pack(color).components(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }, + default_color: to_color(color), }) }, ); diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 644c9f84..69270a73 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -300,10 +300,15 @@ impl Pipeline { wgpu::RenderPassColorAttachment { view: attachment, resolve_target, - ops: wgpu::Operations { load, store: true }, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, }, )], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); let layer = &mut self.layers[layer]; diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 320b5b12..14abd20b 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -167,10 +167,12 @@ impl Blit { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); render_pass.set_pipeline(&self.pipeline); diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 1ddbe5fe..31cf3819 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,8 +6,6 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; - use std::marker::PhantomData; /// A window graphics backend for iced powered by `wgpu`. @@ -26,9 +24,9 @@ impl<Theme> Compositor<Theme> { /// Requests a new [`Compositor`] with the given [`Settings`]. /// /// Returns `None` if no compatible graphics adapter could be found. - pub async fn request<W: HasRawWindowHandle + HasRawDisplayHandle>( + pub async fn request<W: compositor::Window>( settings: Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Option<Self> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.internal_backend, @@ -41,14 +39,15 @@ impl<Theme> Compositor<Theme> { if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance .enumerate_adapters(settings.internal_backend) - .map(|adapter| adapter.get_info()) + .iter() + .map(wgpu::Adapter::get_info) .collect(); log::info!("Available adapters: {available_adapters:#?}"); } #[allow(unsafe_code)] let compatible_surface = compatible_window - .and_then(|window| unsafe { instance.create_surface(window).ok() }); + .and_then(|window| instance.create_surface(window).ok()); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { @@ -100,14 +99,14 @@ impl<Theme> Compositor<Theme> { let (device, queue) = loop { - let limits = limits.next()?; + let required_limits = limits.next()?; let device = adapter.request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), - features: wgpu::Features::empty(), - limits, + required_features: wgpu::Features::empty(), + required_limits, }, None, ).await.ok(); @@ -136,26 +135,24 @@ impl<Theme> Compositor<Theme> { /// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and /// window. -pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( +pub fn new<W: compositor::Window, Theme>( settings: Settings, - compatible_window: Option<&W>, -) -> Result<(Compositor<Theme>, Backend), Error> { + compatible_window: W, +) -> Result<Compositor<Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, - compatible_window, + Some(compatible_window), )) .ok_or(Error::GraphicsAdapterNotFound)?; - let backend = compositor.create_backend(); - - Ok((compositor, backend)) + Ok(compositor) } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. pub fn present<Theme, T: AsRef<str>>( compositor: &mut Compositor<Theme>, backend: &mut Backend, - surface: &mut wgpu::Surface, + surface: &mut wgpu::Surface<'static>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -178,6 +175,7 @@ pub fn present<Theme, T: AsRef<str>>( &compositor.queue, &mut encoder, Some(background_color), + frame.texture.format(), view, primitives, viewport, @@ -208,32 +206,32 @@ pub fn present<Theme, T: AsRef<str>>( impl<Theme> graphics::Compositor for Compositor<Theme> { type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = wgpu::Surface; + type Surface = wgpu::Surface<'static>; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new<W: compositor::Window>( settings: Self::Settings, - compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(settings, compatible_window)?; + compatible_window: W, + ) -> Result<Self, Error> { + new(settings, compatible_window) + } - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + fn create_renderer(&self) -> Self::Renderer { + Renderer::new( + self.create_backend(), + self.settings.default_font, + self.settings.default_text_size, + ) } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface<W: compositor::Window>( &mut self, - window: &W, + window: W, width: u32, height: u32, - ) -> wgpu::Surface { - #[allow(unsafe_code)] - let mut surface = unsafe { self.instance.create_surface(window) } + ) -> Self::Surface { + let mut surface = self + .instance + .create_surface(window) .expect("Create surface"); self.configure_surface(&mut surface, width, height); @@ -257,6 +255,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { height, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); } @@ -357,6 +356,7 @@ pub fn screenshot<Theme, T: AsRef<str>>( &compositor.queue, &mut encoder, Some(background_color), + texture.format(), &view, primitives, viewport, diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 6d62c181..e8e363c4 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -20,6 +20,7 @@ image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] +wgpu = ["iced_renderer/wgpu"] [dependencies] iced_renderer.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 384a3156..0ebb8dcc 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -10,8 +10,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, + Shell, Size, Vector, Widget, }; pub use iced_style::button::{Appearance, StyleSheet}; @@ -71,11 +71,14 @@ where { /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + let content = content.into(); + let size = content.as_widget().size_hint(); + Button { - content: content.into(), + content, on_press: None, - width: Length::Shrink, - height: Length::Shrink, + width: size.width.fluid(), + height: size.height.fluid(), padding: Padding::new(5.0), style: <Renderer::Theme as StyleSheet>::Style::default(), } @@ -149,12 +152,11 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -431,15 +433,7 @@ pub fn layout( padding: Padding, layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - - let mut content = layout_content(&limits.pad(padding)); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()).pad(padding); - - content.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(size, vec![content]) + layout::padded(limits, width, height, padding, layout_content) } /// Returns the [`mouse::Interaction`] of a [`Button`]. diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 390f4d92..4e42a671 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -14,8 +14,9 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::{Clipboard, Element, Shell, Widget}; -use crate::core::{Length, Rectangle, Size, Vector}; +use crate::core::{ + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, +}; use crate::graphics::geometry; use std::marker::PhantomData; @@ -119,12 +120,11 @@ where tree::State::new(P::State::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -133,10 +133,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs index 1288365f..a8eb47f7 100644 --- a/widget/src/canvas/event.rs +++ b/widget/src/canvas/event.rs @@ -8,7 +8,7 @@ pub use crate::core::event::Status; /// A [`Canvas`] event. /// /// [`Canvas`]: crate::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { /// A mouse event. Mouse(mouse::Event), diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index d7fdf339..0353b3ad 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -174,12 +174,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -266,7 +265,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); @@ -315,6 +314,7 @@ where }, bounds.center(), custom_style.icon_color, + *viewport, ); } } @@ -330,6 +330,7 @@ where crate::text::Appearance { color: custom_style.text_color, }, + viewport, ); } } diff --git a/widget/src/column.rs b/widget/src/column.rs index 42e90ac1..d6eea84b 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -7,7 +7,7 @@ use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Widget, + Shell, Size, Widget, }; /// A container that distributes its contents vertically. @@ -22,16 +22,12 @@ pub struct Column<'a, Message, Renderer = crate::Renderer> { children: Vec<Element<'a, Message, Renderer>>, } -impl<'a, Message, Renderer> Column<'a, Message, Renderer> { +impl<'a, Message, Renderer> Column<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ /// Creates an empty [`Column`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: Vec<Element<'a, Message, Renderer>>, - ) -> Self { Column { spacing: 0.0, padding: Padding::ZERO, @@ -39,10 +35,17 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - children, + children: Vec::new(), } } + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, + ) -> Self { + children.into_iter().fold(Self::new(), Self::push) + } + /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -88,12 +91,26 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { mut self, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { - self.children.push(child.into()); + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + + self.children.push(child); self } } -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { +impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ fn default() -> Self { Self::new() } @@ -112,12 +129,11 @@ where tree.diff_children(&self.children); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -126,15 +142,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); + let limits = limits.max_width(self.max_width); layout::flex::resolve( layout::flex::Axis::Vertical, renderer, &limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, @@ -224,15 +239,17 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child - .as_widget() - .draw(state, renderer, theme, style, layout, cursor, viewport); + if let Some(viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, layout, cursor, &viewport, + ); + } } } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 768c2402..73beeac3 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -1,6 +1,7 @@ //! Display a dropdown list of searchable and selectable options. use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -8,7 +9,9 @@ use crate::core::renderer; use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; -use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell}; +use crate::core::{ + Clipboard, Element, Length, Padding, Rectangle, Shell, Size, +}; use crate::overlay::menu; use crate::text::LineHeight; use crate::{container, scrollable, text_input, TextInput}; @@ -297,12 +300,8 @@ where + scrollable::StyleSheet + menu::StyleSheet, { - fn width(&self) -> Length { - Widget::<TextInputEvent, Renderer>::width(&self.text_input) - } - - fn height(&self) -> Length { - Widget::<TextInputEvent, Renderer>::height(&self.text_input) + fn size(&self) -> Size<Length> { + Widget::<TextInputEvent, Renderer>::size(&self.text_input) } fn layout( @@ -438,14 +437,14 @@ where } if let Event::Keyboard(keyboard::Event::KeyPressed { - key_code, + key: keyboard::Key::Named(named_key), modifiers, .. }) = event { let shift_modifer = modifiers.shift(); - match (key_code, shift_modifer) { - (keyboard::KeyCode::Enter, _) => { + match (named_key, shift_modifer) { + (key::Named::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = state.filtered_options.options.get(*index) @@ -457,8 +456,7 @@ where event_status = event::Status::Captured; } - (keyboard::KeyCode::Up, _) - | (keyboard::KeyCode::Tab, true) => { + (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { if let Some(index) = &mut menu.hovered_option { if *index == 0 { *index = state @@ -494,8 +492,8 @@ where event_status = event::Status::Captured; } - (keyboard::KeyCode::Down, _) - | (keyboard::KeyCode::Tab, false) + (key::Named::ArrowDown, _) + | (key::Named::Tab, false) if !modifiers.shift() => { if let Some(index) = &mut menu.hovered_option { @@ -622,7 +620,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_focused = { let text_input_state = tree.children[0] @@ -645,6 +643,7 @@ where layout, cursor, selection, + viewport, ); } diff --git a/widget/src/container.rs b/widget/src/container.rs index ee7a4965..cffb0458 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -46,17 +46,20 @@ where where T: Into<Element<'a, Message, Renderer>>, { + let content = content.into(); + let size = content.as_widget().size_hint(); + Container { id: None, padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, + width: size.width.fluid(), + height: size.height.fluid(), max_width: f32::INFINITY, max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, style: Default::default(), - content: content.into(), + content, } } @@ -152,12 +155,11 @@ where self.content.as_widget().diff(tree); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -252,21 +254,23 @@ where ) { let style = theme.appearance(&self.style); - draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - tree, - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor, - viewport, - ); + if let Some(viewport) = layout.bounds().intersection(viewport) { + draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + tree, + renderer, + theme, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor, + &viewport, + ); + } } fn overlay<'b>( @@ -309,25 +313,20 @@ pub fn layout( vertical_alignment: alignment::Vertical, layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits - .loose() - .max_width(max_width) - .max_height(max_height) - .width(width) - .height(height); - - let mut content = layout_content(&limits.pad(padding).loose()); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()); - - content.move_to(Point::new(padding.left, padding.top)); - content.align( - Alignment::from(horizontal_alignment), - Alignment::from(vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(padding), vec![content]) + layout::positioned( + &limits.max_width(max_width).max_height(max_height), + width, + height, + padding, + |limits| layout_content(&limits.loose()), + |content, size| { + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ) + }, + ) } /// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3c9c2b29..498dd76c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -16,6 +16,7 @@ use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; +use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; @@ -33,7 +34,7 @@ macro_rules! column { $crate::Column::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + $crate::Column::with_children([$($crate::core::Element::from($x)),+]) ); } @@ -46,7 +47,7 @@ macro_rules! row { $crate::Row::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+]) + $crate::Row::with_children([$($crate::core::Element::from($x)),+]) ); } @@ -64,9 +65,12 @@ where } /// Creates a new [`Column`] with the given children. -pub fn column<Message, Renderer>( - children: Vec<Element<'_, Message, Renderer>>, -) -> Column<'_, Message, Renderer> { +pub fn column<'a, Message, Renderer>( + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, +) -> Column<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ Column::with_children(children) } @@ -76,6 +80,7 @@ pub fn keyed_column<'a, Key, Message, Renderer>( ) -> keyed::Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: core::Renderer, { keyed::Column::with_children(children) } @@ -83,9 +88,12 @@ where /// Creates a new [`Row`] with the given children. /// /// [`Row`]: crate::Row -pub fn row<Message, Renderer>( - children: Vec<Element<'_, Message, Renderer>>, -) -> Row<'_, Message, Renderer> { +pub fn row<'a, Message, Renderer>( + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, +) -> Row<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ Row::with_children(children) } @@ -206,6 +214,20 @@ where TextInput::new(placeholder, value) } +/// Creates a new [`TextEditor`]. +/// +/// [`TextEditor`]: crate::TextEditor +pub fn text_editor<Message, Renderer>( + content: &text_editor::Content<Renderer>, +) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: text_editor::StyleSheet, +{ + TextEditor::new(content) +} + /// Creates a new [`Slider`]. /// /// [`Slider`]: crate::Slider @@ -249,7 +271,7 @@ pub fn pick_list<'a, Message, Renderer, T>( on_selected: impl Fn(T) -> Message + 'a, ) -> PickList<'a, T, Message, Renderer> where - T: ToString + Eq + 'static, + T: ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Renderer: core::text::Renderer, Renderer::Theme: pick_list::StyleSheet @@ -370,6 +392,17 @@ where crate::Canvas::new(program) } +/// Creates a new [`Shader`]. +/// +/// [`Shader`]: crate::Shader +#[cfg(feature = "wgpu")] +pub fn shader<Message, P>(program: P) -> crate::Shader<Message, P> +where + P: crate::shader::Program<Message>, +{ + crate::Shader::new(program) +} + /// Focuses the previous focusable widget. pub fn focus_previous<Message>() -> Command<Message> where diff --git a/widget/src/image.rs b/widget/src/image.rs index a0e89920..e906ac13 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -13,7 +13,7 @@ use crate::core::{ use std::hash::Hash; -pub use image::Handle; +pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { @@ -37,6 +37,7 @@ pub struct Image<Handle> { width: Length, height: Length, content_fit: ContentFit, + filter_method: FilterMethod, } impl<Handle> Image<Handle> { @@ -47,6 +48,7 @@ impl<Handle> Image<Handle> { width: Length::Shrink, height: Length::Shrink, content_fit: ContentFit::Contain, + filter_method: FilterMethod::default(), } } @@ -65,11 +67,15 @@ impl<Handle> Image<Handle> { /// Sets the [`ContentFit`] of the [`Image`]. /// /// Defaults to [`ContentFit::Contain`] - pub fn content_fit(self, content_fit: ContentFit) -> Self { - Self { - content_fit, - ..self - } + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + + /// Sets the [`FilterMethod`] of the [`Image`]. + pub fn filter_method(mut self, filter_method: FilterMethod) -> Self { + self.filter_method = filter_method; + self } } @@ -93,7 +99,7 @@ where }; // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.width(width).height(height).resolve(image_size); + let raw_size = limits.resolve(width, height, image_size); // The uncropped size of the image when fit to the bounds above let full_size = content_fit.fit(image_size, raw_size); @@ -119,6 +125,7 @@ pub fn draw<Renderer, Handle>( layout: Layout<'_>, handle: &Handle, content_fit: ContentFit, + filter_method: FilterMethod, ) where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, @@ -141,7 +148,7 @@ pub fn draw<Renderer, Handle>( ..bounds }; - renderer.draw(handle.clone(), drawing_bounds + offset); + renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -157,12 +164,11 @@ where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -191,7 +197,13 @@ where _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw(renderer, layout, &self.handle, self.content_fit); + draw( + renderer, + layout, + &self.handle, + self.content_fit, + self.filter_method, + ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 44624fc8..98080577 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -22,19 +22,21 @@ pub struct Viewer<Handle> { max_scale: f32, scale_step: f32, handle: Handle, + filter_method: image::FilterMethod, } impl<Handle> Viewer<Handle> { /// Creates a new [`Viewer`] with the given [`State`]. pub fn new(handle: Handle) -> Self { Viewer { + handle, padding: 0.0, width: Length::Shrink, height: Length::Shrink, min_scale: 0.25, max_scale: 10.0, scale_step: 0.10, - handle, + filter_method: image::FilterMethod::default(), } } @@ -95,12 +97,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -111,10 +112,11 @@ where ) -> layout::Node { let Size { width, height } = renderer.dimensions(&self.handle); - let mut size = limits - .width(self.width) - .height(self.height) - .resolve(Size::new(width as f32, height as f32)); + let mut size = limits.resolve( + self.width, + self.height, + Size::new(width as f32, height as f32), + ); let expansion_size = if height > width { self.width @@ -329,6 +331,7 @@ where image::Renderer::draw( renderer, self.handle.clone(), + self.filter_method, Rectangle { x: bounds.x, y: bounds.y, diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 0ef82407..7f05a81e 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -8,7 +8,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Widget, + Shell, Size, Widget, }; /// A container that distributes its contents vertically. @@ -30,26 +30,10 @@ where impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: crate::core::Renderer, { /// Creates an empty [`Column`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, - ) -> Self { - let (keys, children) = children.into_iter().fold( - (Vec::new(), Vec::new()), - |(mut keys, mut children), (key, child)| { - keys.push(key); - children.push(child); - - (keys, children) - }, - ); - Column { spacing: 0.0, padding: Padding::ZERO, @@ -57,11 +41,20 @@ where height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - keys, - children, + keys: Vec::new(), + children: Vec::new(), } } + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, + ) -> Self { + children + .into_iter() + .fold(Self::new(), |column, (key, child)| column.push(key, child)) + } + /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -108,8 +101,19 @@ where key: Key, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + self.keys.push(key); - self.children.push(child.into()); + self.children.push(child); self } } @@ -117,6 +121,7 @@ where impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: crate::core::Renderer, { fn default() -> Self { Self::new() @@ -173,12 +178,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -196,6 +200,8 @@ where layout::flex::Axis::Vertical, renderer, &limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 589dd938..e9edbb4c 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -18,7 +18,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::Element; use crate::core::{ - self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, + self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; @@ -142,12 +142,15 @@ where } } - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) + fn size(&self) -> Size<Length> { + self.with_element(|element| element.as_widget().size()) } - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) + fn size_hint(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( @@ -333,9 +336,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index d454b72b..3684e0c9 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -244,12 +244,15 @@ where self.rebuild_element_if_necessary(); } - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) + fn size(&self) -> Size<Length> { + self.with_element(|element| element.as_widget().size()) } - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) + fn size_hint(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( @@ -577,9 +580,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 8ca9cb86..5dc60d52 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -6,6 +6,7 @@ use std::hash::Hash; /// Creates a new [`Lazy`] widget with the given data `Dependency` and a /// closure that can turn this data into a widget tree. +#[cfg(feature = "lazy")] pub fn lazy<'a, Message, Renderer, Dependency, View>( dependency: Dependency, view: impl Fn(&Dependency) -> View + 'a, @@ -19,6 +20,7 @@ where /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. +#[cfg(feature = "lazy")] pub fn component<'a, C, Message, Renderer>( component: C, ) -> Element<'a, Message, Renderer> @@ -37,6 +39,7 @@ where /// The `view` closure will be provided with the current [`Size`] of /// the [`Responsive`] widget and, therefore, can be used to build the /// contents of the widget in a responsive way. +#[cfg(feature = "lazy")] pub fn responsive<'a, Message, Renderer>( f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, ) -> Responsive<'a, Message, Renderer> diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index ed471988..1df0866f 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -6,7 +6,8 @@ use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, + self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, + Widget, }; use crate::horizontal_space; use crate::runtime::overlay::Nested; @@ -134,12 +135,11 @@ where }) } - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Fill, + } } fn layout( @@ -367,9 +367,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 6feb948c..07378d83 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -35,6 +35,7 @@ pub mod scrollable; pub mod slider; pub mod space; pub mod text; +pub mod text_editor; pub mod text_input; pub mod toggler; pub mod tooltip; @@ -86,6 +87,8 @@ pub use space::Space; #[doc(no_inline)] pub use text::Text; #[doc(no_inline)] +pub use text_editor::TextEditor; +#[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] pub use toggler::Toggler; @@ -94,6 +97,13 @@ pub use tooltip::Tooltip; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; +#[cfg(feature = "wgpu")] +pub mod shader; + +#[cfg(feature = "wgpu")] +#[doc(no_inline)] +pub use shader::Shader; + #[cfg(feature = "svg")] pub mod svg; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 3a5b01a3..87cac3a7 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -8,7 +8,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Widget, + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, }; /// Emit messages on mouse events. @@ -110,12 +110,8 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b293f9fa..f83eebea 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -236,6 +236,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -253,15 +254,14 @@ where ) .width(self.width); - let mut node = self.container.layout(self.state, renderer, &limits); + let node = self.container.layout(self.state, renderer, &limits); + let size = node.size(); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) } else { - position - Vector::new(0.0, node.size().height) - }); - - node + position - Vector::new(0.0, size.height) + }) } fn on_event( @@ -342,12 +342,11 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( @@ -358,7 +357,6 @@ where ) -> layout::Node { use std::f32; - let limits = limits.width(Length::Fill).height(Length::Shrink); let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); @@ -371,7 +369,7 @@ where * self.options.len() as f32, ); - limits.resolve(intrinsic) + limits.resolve(Length::Fill, Length::Shrink, intrinsic) }; layout::Node::new(size) @@ -543,6 +541,7 @@ where } else { appearance.text_color }, + *viewport, ); } } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 2d25a543..cf1f0455 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -265,12 +265,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -490,8 +489,7 @@ pub fn layout<Renderer, T>( &layout::Limits, ) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(width, height, Size::ZERO); let regions = node.pane_regions(spacing, size); let children = contents @@ -500,16 +498,14 @@ pub fn layout<Renderer, T>( let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); - let mut node = layout_content( + let node = layout_content( content, tree, renderer, &layout::Limits::new(size, size), ); - node.move_to(Point::new(region.x, region.y)); - - Some(node) + Some(node.move_to(Point::new(region.x, region.y))) }) .collect(); @@ -531,6 +527,8 @@ pub fn update<'a, Message, T: Draggable>( on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, ) -> event::Status { + const DRAG_DEADBAND_DISTANCE: f32 = 10.0; + let mut event_status = event::Status::Ignored; match event { @@ -572,7 +570,6 @@ pub fn update<'a, Message, T: Draggable>( shell, contents, on_click, - on_drag, ); } } @@ -584,7 +581,6 @@ pub fn update<'a, Message, T: Draggable>( shell, contents, on_click, - on_drag, ); } } @@ -637,7 +633,49 @@ pub fn update<'a, Message, T: Draggable>( } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, on_resize)) = on_resize { + if let Some((_, origin)) = action.clicked_pane() { + if let Some(on_drag) = &on_drag { + let bounds = layout.bounds(); + + if let Some(cursor_position) = cursor.position_over(bounds) + { + let mut clicked_region = contents + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + if let Some(((pane, content), layout)) = + clicked_region.next() + { + if content + .can_be_dragged_at(layout, cursor_position) + { + let pane_position = layout.position(); + + let new_origin = cursor_position + - Vector::new( + pane_position.x, + pane_position.y, + ); + + if new_origin.distance(origin) + > DRAG_DEADBAND_DISTANCE + { + *action = state::Action::Dragging { + pane, + origin, + }; + + shell.publish(on_drag(DragEvent::Picked { + pane, + })); + } + } + } + } + } + } else if let Some((_, on_resize)) = on_resize { if let Some((split, _)) = action.picked_split() { let bounds = layout.bounds(); @@ -712,7 +750,6 @@ fn click_pane<'a, Message, T>( shell: &mut Shell<'_, Message>, contents: impl Iterator<Item = (Pane, T)>, on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, - on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, ) where T: Draggable, { @@ -720,23 +757,15 @@ fn click_pane<'a, Message, T>( .zip(layout.children()) .filter(|(_, layout)| layout.bounds().contains(cursor_position)); - if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(((pane, _), layout)) = clicked_region.next() { if let Some(on_click) = &on_click { shell.publish(on_click(pane)); } - if let Some(on_drag) = &on_drag { - if content.can_be_dragged_at(layout, cursor_position) { - let pane_position = layout.position(); - - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); - - *action = state::Action::Dragging { pane, origin }; - - shell.publish(on_drag(DragEvent::Picked { pane })); - } - } + let pane_position = layout.position(); + let origin = + cursor_position - Vector::new(pane_position.x, pane_position.y); + *action = state::Action::Clicking { pane, origin }; } } @@ -749,7 +778,7 @@ pub fn mouse_interaction( spacing: f32, resize_leeway: Option<f32>, ) -> Option<mouse::Interaction> { - if action.picked_pane().is_some() { + if action.clicked_pane().is_some() || action.picked_pane().is_some() { return Some(mouse::Interaction::Grabbing); } diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 826ea663..ee00f186 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -165,7 +165,7 @@ where let title_bar_size = title_bar_layout.size(); - let mut body_layout = self.body.as_widget().layout( + let body_layout = self.body.as_widget().layout( &mut tree.children[0], renderer, &layout::Limits::new( @@ -177,11 +177,12 @@ where ), ); - body_layout.move_to(Point::new(0.0, title_bar_size.height)); - layout::Node::with_children( max_size, - vec![title_bar_layout, body_layout], + vec![ + title_bar_layout, + body_layout.move_to(Point::new(0.0, title_bar_size.height)), + ], ) } else { self.body.as_widget().layout( diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 481cd770..5d1fe254 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -403,6 +403,15 @@ pub enum Action { /// /// [`PaneGrid`]: super::PaneGrid Idle, + /// A [`Pane`] in the [`PaneGrid`] is being clicked. + /// + /// [`PaneGrid`]: super::PaneGrid + Clicking { + /// The [`Pane`] being clicked. + pane: Pane, + /// The starting [`Point`] of the click interaction. + origin: Point, + }, /// A [`Pane`] in the [`PaneGrid`] is being dragged. /// /// [`PaneGrid`]: super::PaneGrid @@ -432,6 +441,14 @@ impl Action { } } + /// Returns the current [`Pane`] that is being clicked, if any. + pub fn clicked_pane(&self) -> Option<(Pane, Point)> { + match *self { + Action::Clicking { pane, origin, .. } => Some((pane, origin)), + _ => None, + } + } + /// Returns the current [`Split`] that is being dragged, if any. pub fn picked_split(&self) -> Option<(Split, Axis)> { match *self { diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index f4dbb6b1..eb21b743 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -217,7 +217,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.pad(self.padding); + let limits = limits.shrink(self.padding); let max_size = limits.max(); let title_layout = self.content.as_widget().layout( @@ -228,8 +228,8 @@ where let title_size = title_layout.size(); - let mut node = if let Some(controls) = &self.controls { - let mut controls_layout = controls.as_widget().layout( + let node = if let Some(controls) = &self.controls { + let controls_layout = controls.as_widget().layout( &mut tree.children[1], renderer, &layout::Limits::new(Size::ZERO, max_size), @@ -240,11 +240,13 @@ where let height = title_size.height.max(controls_size.height); - controls_layout.move_to(Point::new(space_before_controls, 0.0)); - layout::Node::with_children( Size::new(max_size.width, height), - vec![title_layout, controls_layout], + vec![ + title_layout, + controls_layout + .move_to(Point::new(space_before_controls, 0.0)), + ], ) } else { layout::Node::with_children( @@ -253,9 +255,7 @@ where ) }; - node.move_to(Point::new(self.padding.left, self.padding.top)); - - layout::Node::with_children(node.size().pad(self.padding), vec![node]) + layout::Node::container(node, self.padding) } pub(crate) fn operate( diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 27f32907..2e3aab6f 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -45,7 +45,7 @@ where impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> where - T: ToString + Eq, + T: ToString + PartialEq, [T]: ToOwned<Owned = Vec<T>>, Renderer: text::Renderer, Renderer::Theme: StyleSheet @@ -145,7 +145,7 @@ where impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for PickList<'a, T, Message, Renderer> where - T: Clone + ToString + Eq + 'static, + T: Clone + ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Message: 'a, Renderer: text::Renderer + 'a, @@ -164,12 +164,11 @@ where tree::State::new(State::<Renderer::Paragraph>::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -235,7 +234,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); draw( @@ -253,6 +252,7 @@ where &self.handle, &self.style, || tree.state.downcast_ref::<State<Renderer::Paragraph>>(), + viewport, ); } @@ -281,7 +281,7 @@ where impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where - T: Clone + ToString + Eq + 'static, + T: Clone + ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Message: 'a, Renderer: text::Renderer + 'a, @@ -392,7 +392,6 @@ where { use std::f32; - let limits = limits.width(width).height(Length::Shrink).pad(padding); let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = text_size.unwrap_or_else(|| renderer.default_size()); @@ -415,23 +414,17 @@ where for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { let label = option.to_string(); - renderer.update_paragraph( - paragraph, - Text { - content: &label, - ..option_text - }, - ); + paragraph.update(Text { + content: &label, + ..option_text + }); } if let Some(placeholder) = placeholder { - renderer.update_paragraph( - &mut state.placeholder, - Text { - content: placeholder, - ..option_text - }, - ); + state.placeholder.update(Text { + content: placeholder, + ..option_text + }); } let max_width = match width { @@ -456,7 +449,11 @@ where f32::from(text_line_height.to_absolute(text_size)), ); - limits.resolve(intrinsic).pad(padding) + limits + .width(width) + .shrink(padding) + .resolve(width, Length::Shrink, intrinsic) + .expand(padding) }; layout::Node::new(size) @@ -637,6 +634,7 @@ pub fn draw<'a, T, Renderer>( handle: &Handle<Renderer::Font>, style: &<Renderer::Theme as StyleSheet>::Style, state: impl FnOnce() -> &'a State<Renderer::Paragraph>, + viewport: &Rectangle, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -721,6 +719,7 @@ pub fn draw<'a, T, Renderer>( bounds.center_y(), ), style.handle_color, + *viewport, ); } @@ -749,6 +748,7 @@ pub fn draw<'a, T, Renderer>( } else { style.placeholder_color }, + *viewport, ); } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 07de72d5..15f1277b 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -85,12 +85,11 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)) + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), + } } fn layout( @@ -99,13 +98,11 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))); - - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic( + limits, + self.width, + self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), + ) } fn draw( diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 1dc4da7f..a229eb59 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -50,12 +50,11 @@ impl<'a> QRCode<'a> { } impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 57acc033..f91b20b1 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -201,12 +201,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -291,7 +290,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); @@ -349,6 +348,7 @@ where crate::text::Appearance { color: custom_style.text_color, }, + viewport, ); } } diff --git a/widget/src/row.rs b/widget/src/row.rs index 7ca90fbb..90fd2926 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -7,7 +7,7 @@ use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, - Widget, + Size, Widget, }; /// A container that distributes its contents horizontally. @@ -21,26 +21,29 @@ pub struct Row<'a, Message, Renderer = crate::Renderer> { children: Vec<Element<'a, Message, Renderer>>, } -impl<'a, Message, Renderer> Row<'a, Message, Renderer> { +impl<'a, Message, Renderer> Row<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ /// Creates an empty [`Row`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Row`] with the given elements. - pub fn with_children( - children: Vec<Element<'a, Message, Renderer>>, - ) -> Self { Row { spacing: 0.0, padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, align_items: Alignment::Start, - children, + children: Vec::new(), } } + /// Creates a [`Row`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, + ) -> Self { + children.into_iter().fold(Self::new(), Self::push) + } + /// Sets the horizontal spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -80,12 +83,26 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { mut self, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { - self.children.push(child.into()); + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + + self.children.push(child); self } } -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ fn default() -> Self { Self::new() } @@ -104,12 +121,11 @@ where tree.diff_children(&self.children); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -118,12 +134,12 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - layout::flex::resolve( layout::flex::Axis::Horizontal, renderer, - &limits, + limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, @@ -213,15 +229,17 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child - .as_widget() - .draw(state, renderer, theme, style, layout, cursor, viewport); + if let Some(viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, layout, cursor, &viewport, + ); + } } } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index b5c5fa55..cded9cb1 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -62,12 +62,11 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -76,9 +75,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) + layout::atomic(limits, self.width, self.height) } fn draw( diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 49aed2f0..70db490a 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -220,12 +220,11 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -470,28 +469,25 @@ pub fn layout<Renderer>( direction: &Direction, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); - - let content = layout_content(renderer, &child_limits); - let size = limits.resolve(content.size()); + layout::contained(limits, width, height, |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); - layout::Node::with_children(size, vec![content]) + layout_content(renderer, &child_limits) + }) } /// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] diff --git a/widget/src/shader.rs b/widget/src/shader.rs new file mode 100644 index 00000000..16b68c55 --- /dev/null +++ b/widget/src/shader.rs @@ -0,0 +1,216 @@ +//! A custom shader widget for wgpu applications. +mod event; +mod program; + +pub use event::Event; +pub use program::Program; + +use crate::core; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Widget}; +use crate::core::window; +use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; +use crate::renderer::wgpu::primitive::pipeline; + +use std::marker::PhantomData; + +pub use crate::renderer::wgpu::wgpu; +pub use pipeline::{Primitive, Storage}; + +/// A widget which can render custom shaders with Iced's `wgpu` backend. +/// +/// Must be initialized with a [`Program`], which describes the internal widget state & how +/// its [`Program::Primitive`]s are drawn. +#[allow(missing_debug_implementations)] +pub struct Shader<Message, P: Program<Message>> { + width: Length, + height: Length, + program: P, + _message: PhantomData<Message>, +} + +impl<Message, P: Program<Message>> Shader<Message, P> { + /// Create a new custom [`Shader`]. + pub fn new(program: P) -> Self { + Self { + width: Length::Fixed(100.0), + height: Length::Fixed(100.0), + program, + _message: PhantomData, + } + } + + /// Set the `width` of the custom [`Shader`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Set the `height` of the custom [`Shader`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } +} + +impl<P, Message, Renderer> Widget<Message, Renderer> for Shader<Message, P> +where + P: Program<Message>, + Renderer: pipeline::Renderer, +{ + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<P::State>>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.width, self.height) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: crate::core::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let bounds = layout.bounds(); + + let custom_shader_event = match event { + core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + core::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + core::Event::Window(_, window::Event::RedrawRequested(instant)) => { + Some(Event::RedrawRequested(instant)) + } + _ => None, + }; + + if let Some(custom_shader_event) = custom_shader_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = self.program.update( + state, + custom_shader_event, + bounds, + cursor, + shell, + ); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + renderer.draw_pipeline_primitive( + bounds, + self.program.draw(state, cursor_position, bounds), + ); + } +} + +impl<'a, Message, Renderer, P> From<Shader<Message, P>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: pipeline::Renderer, + P: Program<Message> + 'a, +{ + fn from(custom: Shader<Message, P>) -> Element<'a, Message, Renderer> { + Element::new(custom) + } +} + +impl<Message, T> Program<Message> for &T +where + T: Program<Message>, +{ + type State = T::State; + type Primitive = T::Primitive; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + ) -> (event::Status, Option<Message>) { + T::update(self, state, event, bounds, cursor, shell) + } + + fn draw( + &self, + state: &Self::State, + cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + T::draw(self, state, cursor, bounds) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs new file mode 100644 index 00000000..005c8725 --- /dev/null +++ b/widget/src/shader/event.rs @@ -0,0 +1,25 @@ +//! Handle events of a custom shader widget. +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::time::Instant; +use crate::core::touch; + +pub use crate::core::event::Status; + +/// A [`Shader`] event. +/// +/// [`Shader`]: crate::Shader +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), + + /// A window requested a redraw. + RedrawRequested(Instant), +} diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs new file mode 100644 index 00000000..6dd50404 --- /dev/null +++ b/widget/src/shader/program.rs @@ -0,0 +1,62 @@ +use crate::core::event; +use crate::core::mouse; +use crate::core::{Rectangle, Shell}; +use crate::renderer::wgpu::primitive::pipeline; +use crate::shader; + +/// The state and logic of a [`Shader`] widget. +/// +/// A [`Program`] can mutate the internal state of a [`Shader`] widget +/// and produce messages for an application. +/// +/// [`Shader`]: crate::Shader +pub trait Program<Message> { + /// The internal state of the [`Program`]. + type State: Default + 'static; + + /// The type of primitive this [`Program`] can draw. + type Primitive: pipeline::Primitive + 'static; + + /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes + /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a + /// redraw for the window, etc. + /// + /// By default, this method does and returns nothing. + /// + /// [`State`]: Self::State + fn update( + &self, + _state: &mut Self::State, + _event: shader::Event, + _bounds: Rectangle, + _cursor: mouse::Cursor, + _shell: &mut Shell<'_, Message>, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) + } + + /// Draws the [`Primitive`]. + /// + /// [`Primitive`]: Self::Primitive + fn draw( + &self, + state: &Self::State, + cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position is out of + /// bounds of the [`Shader`]'s program. + /// + /// [`Shader`]: crate::Shader + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} diff --git a/widget/src/slider.rs b/widget/src/slider.rs index ac0982c8..1bc94661 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -159,12 +159,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -173,10 +172,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/space.rs b/widget/src/space.rs index e5a8f169..eef990d1 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -45,12 +45,11 @@ impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: core::Renderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -59,9 +58,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) + layout::atomic(limits, self.width, self.height) } fn draw( diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 2d01d1ab..2357cf65 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -96,12 +96,11 @@ where Renderer: svg::Renderer, Renderer::Theme: iced_style::svg::StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( @@ -115,10 +114,7 @@ where let image_size = Size::new(width as f32, height as f32); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits - .width(self.width) - .height(self.height) - .resolve(image_size); + let raw_size = limits.resolve(self.width, self.height, image_size); // The uncropped size of the image when fit to the bounds above let full_size = self.content_fit.fit(image_size, raw_size); @@ -145,7 +141,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let Size { width, height } = renderer.dimensions(&self.handle); @@ -153,6 +149,7 @@ where let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let is_mouse_over = cursor.is_over(bounds); let render = |renderer: &mut Renderer| { let offset = Vector::new( @@ -166,7 +163,11 @@ where ..bounds }; - let appearance = theme.appearance(&self.style); + let appearance = if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.appearance(&self.style) + }; renderer.draw( self.handle.clone(), diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs new file mode 100644 index 00000000..09a0cac0 --- /dev/null +++ b/widget/src/text_editor.rs @@ -0,0 +1,736 @@ +//! Display a multi-line text input for text editing. +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::keyboard::key; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::highlighter::{self, Highlighter}; +use crate::core::text::{self, LineHeight}; +use crate::core::widget::{self, Widget}; +use crate::core::{ + Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Size, + Vector, +}; + +use std::cell::RefCell; +use std::fmt; +use std::ops::DerefMut; +use std::sync::Arc; + +pub use crate::style::text_editor::{Appearance, StyleSheet}; +pub use text::editor::{Action, Edit, Motion}; + +/// A multi-line text input. +#[allow(missing_debug_implementations)] +pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> +where + Highlighter: text::Highlighter, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: &'a Content<Renderer>, + font: Option<Renderer::Font>, + text_size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + padding: Padding, + style: <Renderer::Theme as StyleSheet>::Style, + on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, + highlighter_settings: Highlighter::Settings, + highlighter_format: fn( + &Highlighter::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, +} + +impl<'a, Message, Renderer> + TextEditor<'a, highlighter::PlainText, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates new [`TextEditor`] with the given [`Content`]. + pub fn new(content: &'a Content<Renderer>) -> Self { + Self { + content, + font: None, + text_size: None, + line_height: LineHeight::default(), + width: Length::Fill, + height: Length::Fill, + padding: Padding::new(5.0), + style: Default::default(), + on_edit: None, + highlighter_settings: (), + highlighter_format: |_highlight, _theme| { + highlighter::Format::default() + }, + } + } +} + +impl<'a, Highlighter, Message, Renderer> + TextEditor<'a, Highlighter, Message, Renderer> +where + Highlighter: text::Highlighter, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Sets the message that should be produced when some action is performed in + /// the [`TextEditor`]. + /// + /// If this method is not called, the [`TextEditor`] will be disabled. + pub fn on_action( + mut self, + on_edit: impl Fn(Action) -> Message + 'a, + ) -> Self { + self.on_edit = Some(Box::new(on_edit)); + self + } + + /// Sets the [`Font`] of the [`TextEditor`]. + /// + /// [`Font`]: text::Renderer::Font + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the [`Padding`] of the [`TextEditor`]. + pub fn padding(mut self, padding: impl Into<Padding>) -> Self { + self.padding = padding.into(); + self + } + + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and + /// a strategy to turn its highlights into some text format. + pub fn highlight<H: text::Highlighter>( + self, + settings: H::Settings, + to_format: fn( + &H::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, + ) -> TextEditor<'a, H, Message, Renderer> { + TextEditor { + content: self.content, + font: self.font, + text_size: self.text_size, + line_height: self.line_height, + width: self.width, + height: self.height, + padding: self.padding, + style: self.style, + on_edit: self.on_edit, + highlighter_settings: settings, + highlighter_format: to_format, + } + } + + /// Sets the style of the [`TextEditor`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } +} + +/// The content of a [`TextEditor`]. +pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>) +where + R: text::Renderer; + +struct Internal<R> +where + R: text::Renderer, +{ + editor: R::Editor, + is_dirty: bool, +} + +impl<R> Content<R> +where + R: text::Renderer, +{ + /// Creates an empty [`Content`]. + pub fn new() -> Self { + Self::with_text("") + } + + /// Creates a [`Content`] with the given text. + pub fn with_text(text: &str) -> Self { + Self(RefCell::new(Internal { + editor: R::Editor::with_text(text), + is_dirty: true, + })) + } + + /// Performs an [`Action`] on the [`Content`]. + pub fn perform(&mut self, action: Action) { + let internal = self.0.get_mut(); + + internal.editor.perform(action); + internal.is_dirty = true; + } + + /// Returns the amount of lines of the [`Content`]. + pub fn line_count(&self) -> usize { + self.0.borrow().editor.line_count() + } + + /// Returns the text of the line at the given index, if it exists. + pub fn line( + &self, + index: usize, + ) -> Option<impl std::ops::Deref<Target = str> + '_> { + std::cell::Ref::filter_map(self.0.borrow(), |internal| { + internal.editor.line(index) + }) + .ok() + } + + /// Returns an iterator of the text of the lines in the [`Content`]. + pub fn lines( + &self, + ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> { + struct Lines<'a, Renderer: text::Renderer> { + internal: std::cell::Ref<'a, Internal<Renderer>>, + current: usize, + } + + impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { + type Item = std::cell::Ref<'a, str>; + + fn next(&mut self) -> Option<Self::Item> { + let line = std::cell::Ref::filter_map( + std::cell::Ref::clone(&self.internal), + |internal| internal.editor.line(self.current), + ) + .ok()?; + + self.current += 1; + + Some(line) + } + } + + Lines { + internal: self.0.borrow(), + current: 0, + } + } + + /// Returns the text of the [`Content`]. + /// + /// Lines are joined with `'\n'`. + pub fn text(&self) -> String { + let mut text = self.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push('\n'); + } + + contents.push_str(&line); + + contents + }, + ); + + if !text.ends_with('\n') { + text.push('\n'); + } + + text + } + + /// Returns the selected text of the [`Content`]. + pub fn selection(&self) -> Option<String> { + self.0.borrow().editor.selection() + } + + /// Returns the current cursor position of the [`Content`]. + pub fn cursor_position(&self) -> (usize, usize) { + self.0.borrow().editor.cursor_position() + } +} + +impl<Renderer> Default for Content<Renderer> +where + Renderer: text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<Renderer> fmt::Debug for Content<Renderer> +where + Renderer: text::Renderer, + Renderer::Editor: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let internal = self.0.borrow(); + + f.debug_struct("Content") + .field("editor", &internal.editor) + .field("is_dirty", &internal.is_dirty) + .finish() + } +} + +struct State<Highlighter: text::Highlighter> { + is_focused: bool, + last_click: Option<mouse::Click>, + drag_click: Option<mouse::click::Kind>, + highlighter: RefCell<Highlighter>, + highlighter_settings: Highlighter::Settings, + highlighter_format_address: usize, +} + +impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer> + for TextEditor<'a, Highlighter, Message, Renderer> +where + Highlighter: text::Highlighter, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::<State<Highlighter>>() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State { + is_focused: false, + last_click: None, + drag_click: None, + highlighter: RefCell::new(Highlighter::new( + &self.highlighter_settings, + )), + highlighter_settings: self.highlighter_settings.clone(), + highlighter_format_address: self.highlighter_format as usize, + }) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> iced_renderer::core::layout::Node { + let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + if state.highlighter_format_address != self.highlighter_format as usize + { + state.highlighter.borrow_mut().change_line(0); + + state.highlighter_format_address = self.highlighter_format as usize; + } + + if state.highlighter_settings != self.highlighter_settings { + state + .highlighter + .borrow_mut() + .update(&self.highlighter_settings); + + state.highlighter_settings = self.highlighter_settings.clone(); + } + + internal.editor.update( + limits.shrink(self.padding).max(), + self.font.unwrap_or_else(|| renderer.default_font()), + self.text_size.unwrap_or_else(|| renderer.default_size()), + self.line_height, + state.highlighter.borrow_mut().deref_mut(), + ); + + layout::Node::new(limits.max()) + } + + fn on_event( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(on_edit) = self.on_edit.as_ref() else { + return event::Status::Ignored; + }; + + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + let Some(update) = Update::from_event( + event, + state, + layout.bounds(), + self.padding, + cursor, + ) else { + return event::Status::Ignored; + }; + + match update { + Update::Click(click) => { + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(click.position()) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; + + state.is_focused = true; + state.last_click = Some(click); + state.drag_click = Some(click.kind()); + + shell.publish(on_edit(action)); + } + Update::Unfocus => { + state.is_focused = false; + state.drag_click = None; + } + Update::Release => { + state.drag_click = None; + } + Update::Action(action) => { + shell.publish(on_edit(action)); + } + Update::Copy => { + if let Some(selection) = self.content.selection() { + clipboard.write(selection); + } + } + Update::Paste => { + if let Some(contents) = clipboard.read() { + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(contents), + )))); + } + } + } + + event::Status::Captured + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_ref::<State<Highlighter>>(); + + internal.editor.highlight( + self.font.unwrap_or_else(|| renderer.default_font()), + state.highlighter.borrow_mut().deref_mut(), + |highlight| (self.highlighter_format)(highlight, theme), + ); + + let is_disabled = self.on_edit.is_none(); + let is_mouse_over = cursor.is_over(bounds); + + let appearance = if is_disabled { + theme.disabled(&self.style) + } else if state.is_focused { + theme.focused(&self.style) + } else if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.active(&self.style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + renderer.fill_editor( + &internal.editor, + bounds.position() + + Vector::new(self.padding.left, self.padding.top), + style.text_color, + *viewport, + ); + + let translation = Vector::new( + bounds.x + self.padding.left, + bounds.y + self.padding.top, + ); + + if state.is_focused { + match internal.editor.cursor() { + Cursor::Caret(position) => { + let position = position + translation; + + if bounds.contains(position) { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x, + y: position.y, + width: 1.0, + height: self + .line_height + .to_absolute( + self.text_size.unwrap_or_else( + || renderer.default_size(), + ), + ) + .into(), + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(&self.style), + ); + } + } + Cursor::Selection(ranges) => { + for range in ranges.into_iter().filter_map(|range| { + bounds.intersection(&(range + translation)) + }) { + renderer.fill_quad( + renderer::Quad { + bounds: range, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.selection_color(&self.style), + ); + } + } + } + } + } + + fn mouse_interaction( + &self, + _state: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let is_disabled = self.on_edit.is_none(); + + if cursor.is_over(layout.bounds()) { + if is_disabled { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } + } +} + +impl<'a, Highlighter, Message, Renderer> + From<TextEditor<'a, Highlighter, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Highlighter: text::Highlighter, + Message: 'a, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_editor: TextEditor<'a, Highlighter, Message, Renderer>, + ) -> Self { + Self::new(text_editor) + } +} + +enum Update { + Click(mouse::Click), + Unfocus, + Release, + Action(Action), + Copy, + Paste, +} + +impl Update { + fn from_event<H: Highlighter>( + event: Event, + state: &State<H>, + bounds: Rectangle, + padding: Padding, + cursor: mouse::Cursor, + ) -> Option<Self> { + let action = |action| Some(Update::Action(action)); + let edit = |edit| action(Action::Edit(edit)); + + match event { + Event::Mouse(event) => match event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + if let Some(cursor_position) = cursor.position_in(bounds) { + let cursor_position = cursor_position + - Vector::new(padding.top, padding.left); + + let click = mouse::Click::new( + cursor_position, + state.last_click, + ); + + Some(Update::Click(click)) + } else if state.is_focused { + Some(Update::Unfocus) + } else { + None + } + } + mouse::Event::ButtonReleased(mouse::Button::Left) => { + Some(Update::Release) + } + mouse::Event::CursorMoved { .. } => match state.drag_click { + Some(mouse::click::Kind::Single) => { + let cursor_position = cursor.position_in(bounds)? + - Vector::new(padding.top, padding.left); + + action(Action::Drag(cursor_position)) + } + _ => None, + }, + mouse::Event::WheelScrolled { delta } + if cursor.is_over(bounds) => + { + action(Action::Scroll { + lines: match delta { + mouse::ScrollDelta::Lines { y, .. } => { + if y.abs() > 0.0 { + (y.signum() * -(y.abs() * 4.0).max(1.0)) + as i32 + } else { + 0 + } + } + mouse::ScrollDelta::Pixels { y, .. } => { + (-y / 4.0) as i32 + } + }, + }) + } + _ => None, + }, + Event::Keyboard(event) => match event { + keyboard::Event::KeyPressed { + key, + modifiers, + text, + .. + } if state.is_focused => { + if let keyboard::Key::Named(named_key) = key.as_ref() { + if let Some(motion) = motion(named_key) { + let motion = if platform::is_jump_modifier_pressed( + modifiers, + ) { + motion.widen() + } else { + motion + }; + + return action(if modifiers.shift() { + Action::Select(motion) + } else { + Action::Move(motion) + }); + } + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + edit(Edit::Enter) + } + keyboard::Key::Named(key::Named::Backspace) => { + edit(Edit::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => { + edit(Edit::Delete) + } + keyboard::Key::Named(key::Named::Escape) => { + Some(Self::Unfocus) + } + keyboard::Key::Character("c") + if modifiers.command() => + { + Some(Self::Copy) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + _ => { + let text = text?; + + edit(Edit::Insert( + text.chars().next().unwrap_or_default(), + )) + } + } + } + _ => None, + }, + _ => None, + } + } +} + +fn motion(key: key::Named) -> Option<Motion> { + match key { + key::Named::ArrowLeft => Some(Motion::Left), + key::Named::ArrowRight => Some(Motion::Right), + key::Named::ArrowUp => Some(Motion::Up), + key::Named::ArrowDown => Some(Motion::Down), + key::Named::Home => Some(Motion::Home), + key::Named::End => Some(Motion::End), + key::Named::PageUp => Some(Motion::PageUp), + key::Named::PageDown => Some(Motion::PageDown), + _ => None, + } +} + +mod platform { + use crate::core::keyboard; + + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt() + } else { + modifiers.control() + } + } +} diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9e1fb796..c3dce8be 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -14,6 +14,7 @@ use editor::Editor; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; @@ -238,6 +239,7 @@ where layout: Layout<'_>, cursor: mouse::Cursor, value: Option<&Value>, + viewport: &Rectangle, ) { draw( renderer, @@ -250,6 +252,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } } @@ -281,12 +284,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -362,7 +364,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { draw( renderer, @@ -375,6 +377,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } @@ -503,14 +506,11 @@ where { let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let limits = limits - .width(width) - .pad(padding) - .height(line_height.to_absolute(text_size)); + let height = line_height.to_absolute(text_size); - let text_bounds = limits.resolve(Size::ZERO); + let limits = limits.width(width).shrink(padding); + let text_bounds = limits.resolve(width, height, Size::ZERO); let placeholder_text = Text { font, @@ -523,18 +523,15 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.placeholder, placeholder_text); + state.placeholder.update(placeholder_text); let secure_value = is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); - renderer.update_paragraph( - &mut state.value, - Text { - content: &value.to_string(), - ..placeholder_text - }, - ); + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); if let Some(icon) = icon { let icon_text = Text { @@ -548,45 +545,45 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.icon, icon_text); + state.icon.update(icon_text); let icon_width = state.icon.min_width(); - let mut text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ); - - let mut icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)); - - match icon.side { - Side::Left => { - text_node.move_to(Point::new( + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( padding.left + icon_width + icon.spacing, padding.top, - )); - - icon_node.move_to(Point::new(padding.left, padding.top)); - } - Side::Right => { - text_node.move_to(Point::new(padding.left, padding.top)); - - icon_node.move_to(Point::new( + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( padding.left + text_bounds.width - icon_width, padding.top, - )); - } + ), + ), }; + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + layout::Node::with_children( - text_bounds.pad(padding), + text_bounds.expand(padding), vec![text_node, icon_node], ) } else { - let mut text = layout::Node::new(text_bounds); - text.move_to(Point::new(padding.left, padding.top)); + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); - layout::Node::with_children(text_bounds.pad(padding), vec![text]) + layout::Node::with_children(text_bounds.expand(padding), vec![text]) } } @@ -752,34 +749,7 @@ where return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - if state.is_pasting.is_none() - && !state.keyboard_modifiers.command() - && !c.is_control() - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -790,14 +760,13 @@ where let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); - match key_code { - keyboard::KeyCode::Enter - | keyboard::KeyCode::NumpadEnter => { + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } - keyboard::KeyCode::Backspace => { + keyboard::Key::Named(key::Named::Backspace) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -817,7 +786,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Delete => { + keyboard::Key::Named(key::Named::Delete) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -839,7 +808,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Left => { + keyboard::Key::Named(key::Named::ArrowLeft) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -854,7 +823,7 @@ where state.cursor.move_left(value); } } - keyboard::KeyCode::Right => { + keyboard::Key::Named(key::Named::ArrowRight) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -869,7 +838,7 @@ where state.cursor.move_right(value); } } - keyboard::KeyCode::Home => { + keyboard::Key::Named(key::Named::Home) => { if modifiers.shift() { state .cursor @@ -878,7 +847,7 @@ where state.cursor.move_to(0); } } - keyboard::KeyCode::End => { + keyboard::Key::Named(key::Named::End) => { if modifiers.shift() { state.cursor.select_range( state.cursor.start(value), @@ -888,7 +857,7 @@ where state.cursor.move_to(value.len()); } } - keyboard::KeyCode::C + keyboard::Key::Character("c") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -898,7 +867,7 @@ where .write(value.select(start, end).to_string()); } } - keyboard::KeyCode::X + keyboard::Key::Character("x") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -916,7 +885,7 @@ where update_cache(state, value); } - keyboard::KeyCode::V => { + keyboard::Key::Character("v") => { if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() { @@ -953,12 +922,12 @@ where state.is_pasting = None; } } - keyboard::KeyCode::A + keyboard::Key::Character("a") if state.keyboard_modifiers.command() => { state.cursor.select_all(value); } - keyboard::KeyCode::Escape => { + keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; state.is_dragging = false; state.is_pasting = None; @@ -966,28 +935,55 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } - _ => {} + _ => { + if let Some(text) = text { + let c = text.chars().next().unwrap_or_default(); + + if state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() + { + let mut editor = + Editor::new(value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, value); + + return event::Status::Captured; + } + } + } } return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state(); if state.is_focused.is_some() { - match key_code { - keyboard::KeyCode::V => { + match key.as_ref() { + keyboard::Key::Character("v") => { state.is_pasting = None; } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } _ => {} @@ -1003,14 +999,14 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(window::Event::Unfocused) => { + Event::Window(_, window::Event::Unfocused) => { let state = state(); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(window::Event::Focused) => { + Event::Window(_, window::Event::Focused) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1020,7 +1016,7 @@ where shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(_, window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1058,6 +1054,7 @@ pub fn draw<Renderer>( is_secure: bool, icon: Option<&Icon<Renderer::Font>>, style: &<Renderer::Theme as StyleSheet>::Style, + viewport: &Rectangle, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -1099,6 +1096,7 @@ pub fn draw<Renderer>( &state.icon, icon_layout.bounds().center(), appearance.icon_color, + *viewport, ); } @@ -1192,11 +1190,11 @@ pub fn draw<Renderer>( (None, 0.0) }; - let text_width = state.value.min_width(); - - let render = |renderer: &mut Renderer| { + let draw = |renderer: &mut Renderer, viewport| { if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); + renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { + renderer.fill_quad(cursor, color); + }); } else { renderer.with_translation(Vector::ZERO, |_| {}); } @@ -1207,7 +1205,8 @@ pub fn draw<Renderer>( } else { &state.value }, - Point::new(text_bounds.x, text_bounds.center_y()), + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), if text.is_empty() { theme.placeholder_color(style) } else if is_disabled { @@ -1215,15 +1214,14 @@ pub fn draw<Renderer>( } else { theme.value_color(style) }, + viewport, ); }; - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render); - }); + if cursor.is_some() { + renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { - render(renderer); + draw(renderer, text_bounds); } } @@ -1461,7 +1459,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = renderer.create_paragraph(Text { + state.value = Renderer::Paragraph::with_text(Text { font, line_height, content: &value.to_string(), diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 476c8330..941159ea 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -168,12 +168,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( @@ -266,7 +265,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; @@ -287,6 +286,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance::default(), + viewport, ); } diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index b041d2e9..d09a9255 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -64,6 +64,12 @@ where self } + /// Sets the [`text::Shaping`] strategy of the [`Tooltip`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.tooltip = self.tooltip.shaping(shaping); + self + } + /// Sets the font of the [`Tooltip`]. /// /// [`Font`]: Renderer::Font @@ -125,12 +131,8 @@ where widget::tree::Tag::of::<State>() } - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( @@ -157,11 +159,19 @@ where ) -> event::Status { let state = tree.state.downcast_mut::<State>(); + let was_idle = *state == State::Idle; + *state = cursor .position_over(layout.bounds()) .map(|cursor_position| State::Hovered { cursor_position }) .unwrap_or_default(); + let is_idle = *state == State::Idle; + + if was_idle != is_idle { + shell.invalidate_layout(); + } + self.content.as_widget_mut().on_event( &mut tree.children[0], event, @@ -289,7 +299,7 @@ pub enum Position { Right, } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] enum State { #[default] Idle, @@ -325,6 +335,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let viewport = Rectangle::with_size(bounds); @@ -338,7 +349,7 @@ where .then(|| viewport.size()) .unwrap_or(Size::INFINITY), ) - .pad(Padding::new(self.padding)), + .shrink(Padding::new(self.padding)), ); let text_bounds = text_layout.bounds(); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 01d3359c..a3029d76 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -156,12 +156,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: self.height, + } } fn layout( @@ -170,10 +169,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 674a66d3..87e600ae 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -19,6 +19,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] +multi-window = ["iced_runtime/multi-window"] [dependencies] iced_graphics.workspace = true @@ -26,7 +27,6 @@ iced_runtime.workspace = true iced_style.workspace = true log.workspace = true -raw-window-handle.workspace = true thiserror.workspace = true tracing.workspace = true window_clipboard.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 8105f8d9..09bf63cc 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,4 @@ //! Create interactive, native cross-platform applications. -#[cfg(feature = "trace")] -mod profiler; mod state; pub use state::State; @@ -26,11 +24,7 @@ use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; use std::mem::ManuallyDrop; - -#[cfg(feature = "trace")] -pub use profiler::Profiler; -#[cfg(feature = "trace")] -use tracing::{info_span, instrument::Instrument}; +use std::sync::Arc; /// An interactive, native cross-platform application. /// @@ -119,16 +113,12 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; - #[cfg(feature = "trace")] - let _guard = Profiler::init(); - let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "trace")] - let _ = info_span!("Application", "RUN").entered(); - - let event_loop = EventLoopBuilder::with_user_event().build(); + let event_loop = EventLoopBuilder::with_user_event() + .build() + .expect("Create event loop"); let proxy = event_loop.create_proxy(); let runtime = { @@ -148,26 +138,29 @@ where let target = settings.window.platform_specific.target.clone(); let should_be_visible = settings.window.visible; - let builder = settings - .window - .into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); + let exit_on_close_request = settings.window.exit_on_close_request; + + let builder = conversion::window_settings( + settings.window, + &application.title(), + event_loop.primary_monitor(), + settings.id, + ) + .with_visible(false); log::debug!("Window builder: {builder:#?}"); - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; + let window = Arc::new( + builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?, + ); #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; - let canvas = window.canvas(); + let canvas = window.canvas().expect("Get window canvas"); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -193,75 +186,57 @@ where }; } - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let compositor = C::new(compositor_settings, window.clone())?; + let mut renderer = compositor.create_renderer(); - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); + for font in settings.fonts { + use crate::core::text::Renderer; - let mut instance = Box::pin({ - let run_instance = run_instance::<A, E, C>( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window, - should_be_visible, - settings.exit_on_close_request, - ); + renderer.load_font(font); + } - #[cfg(feature = "trace")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); + let (mut event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); - run_instance - }); + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + renderer, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window, + should_be_visible, + exit_on_close_request, + )); let mut context = task::Context::from_waker(task::noop_waker_ref()); - platform::run(event_loop, move |event, _, control_flow| { - use winit::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { + let _ = event_loop.run(move |event, event_loop| { + if event_loop.exiting() { return; } - 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(), - }; + event_sender.start_send(event).expect("Send event"); - if let Some(event) = event { - event_sender.start_send(event).expect("Send event"); + let poll = instance.as_mut().poll(&mut context); - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { - *control_flow = flow; - } + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + event_loop.set_control_flow(flow); } - task::Poll::Ready(_) => { - *control_flow = ControlFlow::Exit; - } - }; - } - }) + } + task::Poll::Ready(_) => { + event_loop.exit(); + } + }; + }); + + Ok(()) } async fn run_instance<A, E, C>( @@ -272,11 +247,11 @@ async fn run_instance<A, E, C>( mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, A::Message>, + winit::event::Event<A::Message>, >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, - window: winit::window::Window, + window: Arc<winit::window::Window>, should_be_visible: bool, exit_on_close_request: bool, ) where @@ -296,7 +271,7 @@ async fn run_instance<A, E, C>( let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( - &window, + window.clone(), physical_size.width, physical_size.height, ); @@ -340,77 +315,56 @@ async fn run_instance<A, E, C>( while let Some(event) = event_receiver.next().await { match event { - event::Event::NewEvents(start_cause) => { - redraw_pending = matches!( - start_cause, - event::StartCause::Init - | event::StartCause::Poll - | event::StartCause::ResumeTimeReached { .. } - ); + event::Event::NewEvents( + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. }, + ) if !redraw_pending => { + window.request_redraw(); + redraw_pending = true; } - event::Event::MainEventsCleared => { - if !redraw_pending && events.is_empty() && messages.is_empty() { - continue; - } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use crate::core::event; - debug.event_processing_started(); + events.push(Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::WindowEvent { + event: event::WindowEvent::RedrawRequested { .. }, + .. + } => { + let physical_size = state.physical_size(); - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } - debug.event_processing_finished(); + let current_viewport_version = state.viewport_version(); - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } + if viewport_version != current_viewport_version { + let logical_size = state.logical_size(); - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); + debug.layout_started(); + user_interface = ManuallyDrop::new( + ManuallyDrop::into_inner(user_interface) + .relayout(logical_size, &mut renderer), + ); + debug.layout_finished(); - // Update application - update( - &mut application, - &mut compositor, + compositor.configure_surface( &mut surface, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &mut messages, - &window, + physical_size.width, + physical_size.height, ); - // Update window - state.synchronize(&application, &window); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } + viewport_version = current_viewport_version; } // TODO: Avoid redrawing all the time by forcing widgets to @@ -419,6 +373,7 @@ async fn run_instance<A, E, C>( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = Event::Window( + window::Id::MAIN, window::Event::RedrawRequested(Instant::now()), ); @@ -430,6 +385,24 @@ async fn run_instance<A, E, C>( &mut messages, ); + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + window.request_redraw(); + + ControlFlow::Wait + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); + + runtime.broadcast(redraw_event, core::event::Status::Ignored); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -439,6 +412,7 @@ async fn run_instance<A, E, C>( }, state.cursor(), ); + redraw_pending = false; debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -449,88 +423,7 @@ async fn run_instance<A, E, C>( mouse_interaction = new_mouse_interaction; } - window.request_redraw(); - runtime.broadcast(redraw_event, core::event::Status::Ignored); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => ControlFlow::Poll, - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - redraw_pending = false; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use crate::core::event; - - events.push(Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - } - event::Event::RedrawRequested(_) => { - #[cfg(feature = "trace")] - let _ = info_span!("Application", "FRAME").entered(); - - 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 logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - debug.draw_finished(); - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); - - viewport_version = current_viewport_version; - } - match compositor.present( &mut renderer, &mut surface, @@ -571,13 +464,81 @@ async fn run_instance<A, E, C>( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( - &window_event, + window::Id::MAIN, + window_event, state.scale_factor(), state.modifiers(), ) { events.push(event); } } + event::Event::AboutToWait => { + if events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + + let (interface_state, statuses) = user_interface.update( + &events, + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + + debug.event_processing_finished(); + + for (event, status) in + events.drain(..).zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + + if !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated + ) + { + let mut cache = + ManuallyDrop::into_inner(user_interface).into_cache(); + + // Update application + update( + &mut application, + &mut compositor, + &mut surface, + &mut cache, + &mut state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut should_exit, + &mut proxy, + &mut debug, + &mut messages, + &window, + ); + + user_interface = ManuallyDrop::new(build_user_interface( + &application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); + + if should_exit { + break; + } + } + + if !redraw_pending { + window.request_redraw(); + redraw_pending = true; + } + } _ => {} } } @@ -589,8 +550,8 @@ async fn run_instance<A, E, C>( /// Returns true if the provided event should cause an [`Application`] to /// exit. pub fn requests_exit( - event: &winit::event::WindowEvent<'_>, - _modifiers: winit::event::ModifiersState, + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, ) -> bool { use winit::event::WindowEvent; @@ -598,14 +559,14 @@ pub fn requests_exit( WindowEvent::CloseRequested => true, #[cfg(target_os = "macos")] WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), state: winit::event::ElementState::Pressed, .. }, .. - } if _modifiers.logo() => true, + } if c == "q" && _modifiers.super_key() => true, _ => false, } } @@ -622,24 +583,12 @@ pub fn build_user_interface<'a, A: Application>( where <A::Renderer as core::Renderer>::Theme: StyleSheet, { - #[cfg(feature = "trace")] - let view_span = info_span!("Application", "VIEW").entered(); - debug.view_started(); let view = application.view(); - - #[cfg(feature = "trace")] - let _ = view_span.exit(); debug.view_finished(); - #[cfg(feature = "trace")] - let layout_span = info_span!("Application", "LAYOUT").entered(); - debug.layout_started(); let user_interface = UserInterface::build(view, size, cache, renderer); - - #[cfg(feature = "trace")] - let _ = layout_span.exit(); debug.layout_finished(); user_interface @@ -652,7 +601,7 @@ pub fn update<A: Application, C, E: Executor>( compositor: &mut C, surface: &mut C::Surface, cache: &mut user_interface::Cache, - state: &State<A>, + state: &mut State<A>, renderer: &mut A::Renderer, runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, @@ -666,16 +615,10 @@ pub fn update<A: Application, C, E: Executor>( <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { - #[cfg(feature = "trace")] - let update_span = info_span!("Application", "UPDATE").entered(); - debug.log_message(&message); debug.update_started(); let command = runtime.enter(|| application.update(message)); - - #[cfg(feature = "trace")] - let _ = update_span.exit(); debug.update_finished(); run_command( @@ -695,6 +638,8 @@ pub fn update<A: Application, C, E: Executor>( ); } + state.synchronize(application, window); + let subscription = application.subscription(); runtime.track(subscription.into_recipes()); } @@ -729,6 +674,9 @@ pub fn run_command<A, C, E>( command::Action::Future(future) => { runtime.spawn(future); } + command::Action::Stream(stream) => { + runtime.run(stream); + } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { let message = tag(clipboard.read()); @@ -742,20 +690,28 @@ pub fn run_command<A, C, E>( } }, command::Action::Window(action) => match action { - window::Action::Close => { + window::Action::Close(_id) => { *should_exit = true; } - window::Action::Drag => { + window::Action::Drag(_id) => { let _res = window.drag_window(); } - window::Action::Resize(size) => { - window.set_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); + window::Action::Spawn { .. } => { + log::warn!( + "Spawning a window is only available with \ + multi-window applications." + ); } - window::Action::FetchSize(callback) => { - let size = window.inner_size(); + window::Action::Resize(_id, size) => { + let _ = + window.request_inner_size(winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }); + } + window::Action::FetchSize(_id, callback) => { + let size = + window.inner_size().to_logical(window.scale_factor()); proxy .send_event(callback(Size::new( @@ -764,29 +720,39 @@ pub fn run_command<A, C, E>( ))) .expect("Send message to event loop"); } - window::Action::Maximize(maximized) => { + window::Action::FetchMaximized(_id, callback) => { + proxy + .send_event(callback(window.is_maximized())) + .expect("Send message to event loop"); + } + window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } - window::Action::Minimize(minimized) => { + window::Action::FetchMinimized(_id, callback) => { + proxy + .send_event(callback(window.is_minimized())) + .expect("Send message to event loop"); + } + window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); } - window::Action::Move { x, y } => { + window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, + x: position.x, + y: position.y, }); } - window::Action::ChangeMode(mode) => { + window::Action::ChangeMode(_id, mode) => { window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( window.current_monitor(), mode, )); } - window::Action::ChangeIcon(icon) => { + window::Action::ChangeIcon(_id, icon) => { window.set_window_icon(conversion::icon(icon)); } - window::Action::FetchMode(tag) => { + window::Action::FetchMode(_id, tag) => { let mode = if window.is_visible().unwrap_or(true) { conversion::mode(window.fullscreen()) } else { @@ -797,29 +763,29 @@ pub fn run_command<A, C, E>( .send_event(tag(mode)) .expect("Send message to event loop"); } - window::Action::ToggleMaximize => { + window::Action::ToggleMaximize(_id) => { window.set_maximized(!window.is_maximized()); } - window::Action::ToggleDecorations => { + window::Action::ToggleDecorations(_id) => { window.set_decorations(!window.is_decorated()); } - window::Action::RequestUserAttention(user_attention) => { + window::Action::RequestUserAttention(_id, user_attention) => { window.request_user_attention( user_attention.map(conversion::user_attention), ); } - window::Action::GainFocus => { + window::Action::GainFocus(_id) => { window.focus_window(); } - window::Action::ChangeLevel(level) => { + window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => { + window::Action::FetchId(_id, tag) => { proxy .send_event(tag(window.id().into())) .expect("Send message to event loop"); } - window::Action::Screenshot(tag) => { + window::Action::Screenshot(_id, tag) => { let bytes = compositor.screenshot( renderer, surface, @@ -900,43 +866,3 @@ pub fn run_command<A, C, E>( } } } - -#[cfg(not(target_arch = "wasm32"))] -mod platform { - pub fn run<T, F>( - mut event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> Result<(), super::Error> - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - use winit::platform::run_return::EventLoopExtRunReturn; - - let _ = event_loop.run_return(event_handler); - - Ok(()) - } -} - -#[cfg(target_arch = "wasm32")] -mod platform { - pub fn run<T, F>( - event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> ! - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - event_loop.run(event_handler) - } -} diff --git a/winit/src/application/profiler.rs b/winit/src/application/profiler.rs deleted file mode 100644 index 7031507a..00000000 --- a/winit/src/application/profiler.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! A simple profiler for Iced. -use std::ffi::OsStr; -use std::path::Path; -use std::time::Duration; -use tracing_subscriber::prelude::*; -use tracing_subscriber::Registry; -#[cfg(feature = "chrome-trace")] -use { - tracing_chrome::FlushGuard, - tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}, -}; - -/// Profiler state. This will likely need to be updated or reworked when adding new tracing backends. -#[allow(missing_debug_implementations)] -pub struct Profiler { - #[cfg(feature = "chrome-trace")] - /// [`FlushGuard`] must not be dropped until the application scope is dropped for accurate tracing. - _guard: FlushGuard, -} - -impl Profiler { - /// Initializes the [`Profiler`]. - pub fn init() -> Self { - // Registry stores the spans & generates unique span IDs - let subscriber = Registry::default(); - - let default_path = Path::new(env!("CARGO_MANIFEST_DIR")); - let curr_exe = std::env::current_exe() - .unwrap_or_else(|_| default_path.to_path_buf()); - let out_dir = curr_exe.parent().unwrap_or(default_path).join("traces"); - - #[cfg(feature = "chrome-trace")] - let (chrome_layer, guard) = { - let mut layer = tracing_chrome::ChromeLayerBuilder::new(); - - // Optional configurable env var: CHROME_TRACE_FILE=/path/to/trace_file/file.json, - // for uploading to chrome://tracing (old) or ui.perfetto.dev (new). - if let Ok(path) = std::env::var("CHROME_TRACE_FILE") { - layer = layer.file(path); - } else if std::fs::create_dir_all(&out_dir).is_ok() { - let time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(Duration::from_millis(0)) - .as_millis(); - - let curr_exe_name = curr_exe - .file_name() - .unwrap_or_else(|| OsStr::new("trace")) - .to_str() - .unwrap_or("trace"); - - let path = - out_dir.join(format!("{curr_exe_name}_trace_{time}.json")); - - layer = layer.file(path); - } else { - layer = layer.file(env!("CARGO_MANIFEST_DIR")) - } - - let (chrome_layer, guard) = layer - .name_fn(Box::new(|event_or_span| match event_or_span { - tracing_chrome::EventOrSpan::Event(event) => { - event.metadata().name().into() - } - tracing_chrome::EventOrSpan::Span(span) => { - if let Some(fields) = span - .extensions() - .get::<FormattedFields<DefaultFields>>() - { - format!( - "{}: {}", - span.metadata().name(), - fields.fields.as_str() - ) - } else { - span.metadata().name().into() - } - } - })) - .build(); - - (chrome_layer, guard) - }; - - let fmt_layer = tracing_subscriber::fmt::Layer::default(); - let subscriber = subscriber.with(fmt_layer); - - #[cfg(feature = "chrome-trace")] - let subscriber = subscriber.with(chrome_layer); - - // create dispatcher which will forward span events to the subscriber - // this can only be set once or will panic - tracing::subscriber::set_global_default(subscriber) - .expect("Tracer could not set the global default subscriber."); - - Profiler { - #[cfg(feature = "chrome-trace")] - _guard: guard, - } - } -} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index e655529a..8c9b20e0 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -22,7 +22,7 @@ where viewport: Viewport, viewport_version: usize, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, theme: <A::Renderer as core::Renderer>::Theme, appearance: application::Appearance, application: PhantomData<A>, @@ -54,7 +54,7 @@ where viewport, viewport_version: 0, cursor_position: None, - modifiers: winit::event::ModifiersState::default(), + modifiers: winit::keyboard::ModifiersState::default(), theme, appearance, application: PhantomData, @@ -102,7 +102,7 @@ where } /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::event::ModifiersState { + pub fn modifiers(&self) -> winit::keyboard::ModifiersState { self.modifiers } @@ -126,7 +126,7 @@ where pub fn update( &mut self, window: &Window, - event: &WindowEvent<'_>, + event: &WindowEvent, _debug: &mut Debug, ) { match event { @@ -142,10 +142,9 @@ where } WindowEvent::ScaleFactorChanged { scale_factor: new_scale_factor, - new_inner_size, + .. } => { - let size = - Size::new(new_inner_size.width, new_inner_size.height); + let size = self.viewport.physical_size(); self.viewport = Viewport::with_physical_size( size, @@ -164,13 +163,16 @@ where self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = *new_modifiers; + self.modifiers = new_modifiers.state(); } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), state: winit::event::ElementState::Pressed, .. }, diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index f7a32868..8f5c5e63 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -15,7 +15,8 @@ enum State { impl Clipboard { /// Creates a new [`Clipboard`] for the given window. pub fn connect(window: &winit::window::Window) -> Clipboard { - let state = window_clipboard::Clipboard::connect(window) + #[allow(unsafe_code)] + let state = unsafe { window_clipboard::Clipboard::connect(window) } .ok() .map(State::Connected) .unwrap_or(State::Unavailable); diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 3ecd044c..90a5d27f 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -6,14 +6,131 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; use crate::core::window; -use crate::core::{Event, Point}; -use crate::Position; +use crate::core::{Event, Point, Size}; + +/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`. +pub fn window_settings( + settings: window::Settings, + title: &str, + primary_monitor: Option<winit::monitor::MonitorHandle>, + _id: Option<String>, +) -> winit::window::WindowBuilder { + let mut window_builder = winit::window::WindowBuilder::new(); + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { + width: settings.size.width, + height: settings.size.height, + }) + .with_resizable(settings.resizable) + .with_enabled_buttons(if settings.resizable { + winit::window::WindowButtons::all() + } else { + winit::window::WindowButtons::CLOSE + | winit::window::WindowButtons::MINIMIZE + }) + .with_decorations(settings.decorations) + .with_transparent(settings.transparent) + .with_window_icon(settings.icon.and_then(icon)) + .with_window_level(window_level(settings.level)) + .with_visible(settings.visible); + + if let Some(position) = + position(primary_monitor.as_ref(), settings.size, settings.position) + { + window_builder = window_builder.with_position(position); + } + + if let Some(min_size) = settings.min_size { + window_builder = + window_builder.with_min_inner_size(winit::dpi::LogicalSize { + width: min_size.width, + height: min_size.height, + }); + } + + if let Some(max_size) = settings.max_size { + window_builder = + window_builder.with_max_inner_size(winit::dpi::LogicalSize { + width: max_size.width, + height: max_size.height, + }); + } + + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do + // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. + use ::winit::platform::wayland::WindowBuilderExtWayland; + + if let Some(id) = _id { + window_builder = window_builder.with_name(id.clone(), id); + } + } + + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + #[allow(unsafe_code)] + unsafe { + window_builder = window_builder + .with_parent_window(settings.platform_specific.parent); + } + window_builder = window_builder + .with_drag_and_drop(settings.platform_specific.drag_and_drop); + } + + #[cfg(target_os = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS; + + window_builder = window_builder + .with_title_hidden(settings.platform_specific.title_hidden) + .with_titlebar_transparent( + settings.platform_specific.titlebar_transparent, + ) + .with_fullsize_content_view( + settings.platform_specific.fullsize_content_view, + ); + } + + #[cfg(target_os = "linux")] + { + #[cfg(feature = "x11")] + { + use winit::platform::x11::WindowBuilderExtX11; + + window_builder = window_builder.with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); + } + #[cfg(feature = "wayland")] + { + use winit::platform::wayland::WindowBuilderExtWayland; + + window_builder = window_builder.with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); + } + } + + window_builder +} /// Converts a winit window event into an iced event. pub fn window_event( - event: &winit::event::WindowEvent<'_>, + id: window::Id, + event: winit::event::WindowEvent, scale_factor: f64, - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, ) -> Option<Event> { use winit::event::WindowEvent; @@ -21,21 +138,16 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) - } - WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { - let logical_size = new_inner_size.to_logical(scale_factor); - - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::CloseRequested => { - Some(Event::Window(window::Event::CloseRequested)) + Some(Event::Window(id, window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::<f64>(scale_factor); @@ -51,7 +163,7 @@ pub fn window_event( Some(Event::Mouse(mouse::Event::CursorLeft)) } WindowEvent::MouseInput { button, state, .. } => { - let button = mouse_button(*button); + let button = mouse_button(button); Some(Event::Mouse(match state { winit::event::ElementState::Pressed => { @@ -66,8 +178,8 @@ pub fn window_event( winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => { Some(Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { - x: *delta_x, - y: *delta_y, + x: delta_x, + y: delta_y, }, })) } @@ -80,61 +192,81 @@ pub fn window_event( })) } }, - WindowEvent::ReceivedCharacter(c) if !is_private_use_character(*c) => { - Some(Event::Keyboard(keyboard::Event::CharacterReceived(*c))) - } WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(virtual_keycode), + event: + winit::event::KeyEvent { + logical_key, state, + text, + location, .. }, .. } => Some(Event::Keyboard({ - let key_code = key_code(*virtual_keycode); + let key = key(logical_key); let modifiers = self::modifiers(modifiers); + let location = match location { + winit::keyboard::KeyLocation::Standard => { + keyboard::Location::Standard + } + winit::keyboard::KeyLocation::Left => keyboard::Location::Left, + winit::keyboard::KeyLocation::Right => { + keyboard::Location::Right + } + winit::keyboard::KeyLocation::Numpad => { + keyboard::Location::Numpad + } + }; + match state { winit::event::ElementState::Pressed => { keyboard::Event::KeyPressed { - key_code, + key, modifiers, + location, + text, } } winit::event::ElementState::Released => { keyboard::Event::KeyReleased { - key_code, + key, modifiers, + location, } } } })), - WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( - keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), + WindowEvent::ModifiersChanged(new_modifiers) => { + Some(Event::Keyboard(keyboard::Event::ModifiersChanged( + self::modifiers(new_modifiers.state()), + ))) + } + WindowEvent::Focused(focused) => Some(Event::Window( + id, + if focused { + window::Event::Focused + } else { + window::Event::Unfocused + }, )), - WindowEvent::Focused(focused) => Some(Event::Window(if *focused { - window::Event::Focused - } else { - window::Event::Unfocused - })), WindowEvent::HoveredFile(path) => { - Some(Event::Window(window::Event::FileHovered(path.clone()))) + Some(Event::Window(id, window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(window::Event::FileDropped(path.clone()))) + Some(Event::Window(id, window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(window::Event::FilesHoveredLeft)) + Some(Event::Window(id, window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { - Some(Event::Touch(touch_event(*touch, scale_factor))) + 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 })) + Some(Event::Window(id, window::Event::Moved { x, y })) } _ => None, } @@ -153,23 +285,23 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel { } } -/// Converts a [`Position`] to a [`winit`] logical position for a given monitor. +/// Converts a [`window::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, + size: Size, + position: window::Position, ) -> Option<winit::dpi::Position> { match position { - Position::Default => None, - Position::Specific(x, y) => { + window::Position::Default => None, + window::Position::Specific(position) => { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { - x: f64::from(x), - y: f64::from(y), + x: f64::from(position.x), + y: f64::from(position.y), })) } - Position::Centered => { + window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); @@ -178,8 +310,8 @@ pub fn position( 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, + x: (resolution.width - f64::from(size.width)) / 2.0, + y: (resolution.height - f64::from(size.height)) / 2.0, } .to_physical(monitor.scale_factor()); @@ -239,7 +371,7 @@ pub fn mouse_interaction( match interaction { Interaction::Idle => winit::window::CursorIcon::Default, - Interaction::Pointer => winit::window::CursorIcon::Hand, + Interaction::Pointer => winit::window::CursorIcon::Pointer, Interaction::Working => winit::window::CursorIcon::Progress, Interaction::Grab => winit::window::CursorIcon::Grab, Interaction::Grabbing => winit::window::CursorIcon::Grabbing, @@ -262,6 +394,8 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Right => mouse::Button::Right, winit::event::MouseButton::Middle => mouse::Button::Middle, + winit::event::MouseButton::Back => mouse::Button::Back, + winit::event::MouseButton::Forward => mouse::Button::Forward, winit::event::MouseButton::Other(other) => mouse::Button::Other(other), } } @@ -272,14 +406,14 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 pub fn modifiers( - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, ) -> keyboard::Modifiers { 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.set(keyboard::Modifiers::SHIFT, modifiers.shift_key()); + result.set(keyboard::Modifiers::CTRL, modifiers.control_key()); + result.set(keyboard::Modifiers::ALT, modifiers.alt_key()); + result.set(keyboard::Modifiers::LOGO, modifiers.super_key()); result } @@ -329,179 +463,328 @@ pub fn touch_event( /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 -pub fn key_code( - virtual_keycode: winit::event::VirtualKeyCode, -) -> keyboard::KeyCode { - use keyboard::KeyCode; - - match virtual_keycode { - winit::event::VirtualKeyCode::Key1 => KeyCode::Key1, - winit::event::VirtualKeyCode::Key2 => KeyCode::Key2, - winit::event::VirtualKeyCode::Key3 => KeyCode::Key3, - winit::event::VirtualKeyCode::Key4 => KeyCode::Key4, - winit::event::VirtualKeyCode::Key5 => KeyCode::Key5, - winit::event::VirtualKeyCode::Key6 => KeyCode::Key6, - winit::event::VirtualKeyCode::Key7 => KeyCode::Key7, - winit::event::VirtualKeyCode::Key8 => KeyCode::Key8, - winit::event::VirtualKeyCode::Key9 => KeyCode::Key9, - winit::event::VirtualKeyCode::Key0 => KeyCode::Key0, - winit::event::VirtualKeyCode::A => KeyCode::A, - winit::event::VirtualKeyCode::B => KeyCode::B, - winit::event::VirtualKeyCode::C => KeyCode::C, - winit::event::VirtualKeyCode::D => KeyCode::D, - winit::event::VirtualKeyCode::E => KeyCode::E, - winit::event::VirtualKeyCode::F => KeyCode::F, - winit::event::VirtualKeyCode::G => KeyCode::G, - winit::event::VirtualKeyCode::H => KeyCode::H, - winit::event::VirtualKeyCode::I => KeyCode::I, - winit::event::VirtualKeyCode::J => KeyCode::J, - winit::event::VirtualKeyCode::K => KeyCode::K, - winit::event::VirtualKeyCode::L => KeyCode::L, - winit::event::VirtualKeyCode::M => KeyCode::M, - winit::event::VirtualKeyCode::N => KeyCode::N, - winit::event::VirtualKeyCode::O => KeyCode::O, - winit::event::VirtualKeyCode::P => KeyCode::P, - winit::event::VirtualKeyCode::Q => KeyCode::Q, - winit::event::VirtualKeyCode::R => KeyCode::R, - winit::event::VirtualKeyCode::S => KeyCode::S, - winit::event::VirtualKeyCode::T => KeyCode::T, - winit::event::VirtualKeyCode::U => KeyCode::U, - winit::event::VirtualKeyCode::V => KeyCode::V, - winit::event::VirtualKeyCode::W => KeyCode::W, - winit::event::VirtualKeyCode::X => KeyCode::X, - winit::event::VirtualKeyCode::Y => KeyCode::Y, - winit::event::VirtualKeyCode::Z => KeyCode::Z, - winit::event::VirtualKeyCode::Escape => KeyCode::Escape, - winit::event::VirtualKeyCode::F1 => KeyCode::F1, - winit::event::VirtualKeyCode::F2 => KeyCode::F2, - winit::event::VirtualKeyCode::F3 => KeyCode::F3, - winit::event::VirtualKeyCode::F4 => KeyCode::F4, - winit::event::VirtualKeyCode::F5 => KeyCode::F5, - winit::event::VirtualKeyCode::F6 => KeyCode::F6, - winit::event::VirtualKeyCode::F7 => KeyCode::F7, - winit::event::VirtualKeyCode::F8 => KeyCode::F8, - winit::event::VirtualKeyCode::F9 => KeyCode::F9, - winit::event::VirtualKeyCode::F10 => KeyCode::F10, - winit::event::VirtualKeyCode::F11 => KeyCode::F11, - winit::event::VirtualKeyCode::F12 => KeyCode::F12, - winit::event::VirtualKeyCode::F13 => KeyCode::F13, - winit::event::VirtualKeyCode::F14 => KeyCode::F14, - winit::event::VirtualKeyCode::F15 => KeyCode::F15, - winit::event::VirtualKeyCode::F16 => KeyCode::F16, - winit::event::VirtualKeyCode::F17 => KeyCode::F17, - winit::event::VirtualKeyCode::F18 => KeyCode::F18, - winit::event::VirtualKeyCode::F19 => KeyCode::F19, - winit::event::VirtualKeyCode::F20 => KeyCode::F20, - winit::event::VirtualKeyCode::F21 => KeyCode::F21, - winit::event::VirtualKeyCode::F22 => KeyCode::F22, - winit::event::VirtualKeyCode::F23 => KeyCode::F23, - winit::event::VirtualKeyCode::F24 => KeyCode::F24, - winit::event::VirtualKeyCode::Snapshot => KeyCode::Snapshot, - winit::event::VirtualKeyCode::Scroll => KeyCode::Scroll, - winit::event::VirtualKeyCode::Pause => KeyCode::Pause, - winit::event::VirtualKeyCode::Insert => KeyCode::Insert, - winit::event::VirtualKeyCode::Home => KeyCode::Home, - winit::event::VirtualKeyCode::Delete => KeyCode::Delete, - winit::event::VirtualKeyCode::End => KeyCode::End, - winit::event::VirtualKeyCode::PageDown => KeyCode::PageDown, - winit::event::VirtualKeyCode::PageUp => KeyCode::PageUp, - winit::event::VirtualKeyCode::Left => KeyCode::Left, - winit::event::VirtualKeyCode::Up => KeyCode::Up, - winit::event::VirtualKeyCode::Right => KeyCode::Right, - winit::event::VirtualKeyCode::Down => KeyCode::Down, - winit::event::VirtualKeyCode::Back => KeyCode::Backspace, - winit::event::VirtualKeyCode::Return => KeyCode::Enter, - winit::event::VirtualKeyCode::Space => KeyCode::Space, - winit::event::VirtualKeyCode::Compose => KeyCode::Compose, - winit::event::VirtualKeyCode::Caret => KeyCode::Caret, - winit::event::VirtualKeyCode::Numlock => KeyCode::Numlock, - winit::event::VirtualKeyCode::Numpad0 => KeyCode::Numpad0, - winit::event::VirtualKeyCode::Numpad1 => KeyCode::Numpad1, - winit::event::VirtualKeyCode::Numpad2 => KeyCode::Numpad2, - winit::event::VirtualKeyCode::Numpad3 => KeyCode::Numpad3, - winit::event::VirtualKeyCode::Numpad4 => KeyCode::Numpad4, - winit::event::VirtualKeyCode::Numpad5 => KeyCode::Numpad5, - winit::event::VirtualKeyCode::Numpad6 => KeyCode::Numpad6, - winit::event::VirtualKeyCode::Numpad7 => KeyCode::Numpad7, - winit::event::VirtualKeyCode::Numpad8 => KeyCode::Numpad8, - winit::event::VirtualKeyCode::Numpad9 => KeyCode::Numpad9, - winit::event::VirtualKeyCode::AbntC1 => KeyCode::AbntC1, - winit::event::VirtualKeyCode::AbntC2 => KeyCode::AbntC2, - winit::event::VirtualKeyCode::NumpadAdd => KeyCode::NumpadAdd, - winit::event::VirtualKeyCode::Plus => KeyCode::Plus, - winit::event::VirtualKeyCode::Apostrophe => KeyCode::Apostrophe, - winit::event::VirtualKeyCode::Apps => KeyCode::Apps, - winit::event::VirtualKeyCode::At => KeyCode::At, - winit::event::VirtualKeyCode::Ax => KeyCode::Ax, - winit::event::VirtualKeyCode::Backslash => KeyCode::Backslash, - winit::event::VirtualKeyCode::Calculator => KeyCode::Calculator, - winit::event::VirtualKeyCode::Capital => KeyCode::Capital, - winit::event::VirtualKeyCode::Colon => KeyCode::Colon, - winit::event::VirtualKeyCode::Comma => KeyCode::Comma, - winit::event::VirtualKeyCode::Convert => KeyCode::Convert, - winit::event::VirtualKeyCode::NumpadDecimal => KeyCode::NumpadDecimal, - winit::event::VirtualKeyCode::NumpadDivide => KeyCode::NumpadDivide, - winit::event::VirtualKeyCode::Equals => KeyCode::Equals, - winit::event::VirtualKeyCode::Grave => KeyCode::Grave, - winit::event::VirtualKeyCode::Kana => KeyCode::Kana, - winit::event::VirtualKeyCode::Kanji => KeyCode::Kanji, - winit::event::VirtualKeyCode::LAlt => KeyCode::LAlt, - winit::event::VirtualKeyCode::LBracket => KeyCode::LBracket, - winit::event::VirtualKeyCode::LControl => KeyCode::LControl, - winit::event::VirtualKeyCode::LShift => KeyCode::LShift, - winit::event::VirtualKeyCode::LWin => KeyCode::LWin, - winit::event::VirtualKeyCode::Mail => KeyCode::Mail, - winit::event::VirtualKeyCode::MediaSelect => KeyCode::MediaSelect, - winit::event::VirtualKeyCode::MediaStop => KeyCode::MediaStop, - winit::event::VirtualKeyCode::Minus => KeyCode::Minus, - winit::event::VirtualKeyCode::NumpadMultiply => KeyCode::NumpadMultiply, - winit::event::VirtualKeyCode::Mute => KeyCode::Mute, - winit::event::VirtualKeyCode::MyComputer => KeyCode::MyComputer, - winit::event::VirtualKeyCode::NavigateForward => { - KeyCode::NavigateForward - } - winit::event::VirtualKeyCode::NavigateBackward => { - KeyCode::NavigateBackward +pub fn key(key: winit::keyboard::Key) -> keyboard::Key { + use keyboard::key::Named; + use winit::keyboard::NamedKey; + + match key { + winit::keyboard::Key::Character(c) => keyboard::Key::Character(c), + winit::keyboard::Key::Named(named_key) => { + keyboard::Key::Named(match named_key { + NamedKey::Alt => Named::Alt, + NamedKey::AltGraph => Named::AltGraph, + NamedKey::CapsLock => Named::CapsLock, + NamedKey::Control => Named::Control, + NamedKey::Fn => Named::Fn, + NamedKey::FnLock => Named::FnLock, + NamedKey::NumLock => Named::NumLock, + NamedKey::ScrollLock => Named::ScrollLock, + NamedKey::Shift => Named::Shift, + NamedKey::Symbol => Named::Symbol, + NamedKey::SymbolLock => Named::SymbolLock, + NamedKey::Meta => Named::Meta, + NamedKey::Hyper => Named::Hyper, + NamedKey::Super => Named::Super, + NamedKey::Enter => Named::Enter, + NamedKey::Tab => Named::Tab, + NamedKey::Space => Named::Space, + NamedKey::ArrowDown => Named::ArrowDown, + NamedKey::ArrowLeft => Named::ArrowLeft, + NamedKey::ArrowRight => Named::ArrowRight, + NamedKey::ArrowUp => Named::ArrowUp, + NamedKey::End => Named::End, + NamedKey::Home => Named::Home, + NamedKey::PageDown => Named::PageDown, + NamedKey::PageUp => Named::PageUp, + NamedKey::Backspace => Named::Backspace, + NamedKey::Clear => Named::Clear, + NamedKey::Copy => Named::Copy, + NamedKey::CrSel => Named::CrSel, + NamedKey::Cut => Named::Cut, + NamedKey::Delete => Named::Delete, + NamedKey::EraseEof => Named::EraseEof, + NamedKey::ExSel => Named::ExSel, + NamedKey::Insert => Named::Insert, + NamedKey::Paste => Named::Paste, + NamedKey::Redo => Named::Redo, + NamedKey::Undo => Named::Undo, + NamedKey::Accept => Named::Accept, + NamedKey::Again => Named::Again, + NamedKey::Attn => Named::Attn, + NamedKey::Cancel => Named::Cancel, + NamedKey::ContextMenu => Named::ContextMenu, + NamedKey::Escape => Named::Escape, + NamedKey::Execute => Named::Execute, + NamedKey::Find => Named::Find, + NamedKey::Help => Named::Help, + NamedKey::Pause => Named::Pause, + NamedKey::Play => Named::Play, + NamedKey::Props => Named::Props, + NamedKey::Select => Named::Select, + NamedKey::ZoomIn => Named::ZoomIn, + NamedKey::ZoomOut => Named::ZoomOut, + NamedKey::BrightnessDown => Named::BrightnessDown, + NamedKey::BrightnessUp => Named::BrightnessUp, + NamedKey::Eject => Named::Eject, + NamedKey::LogOff => Named::LogOff, + NamedKey::Power => Named::Power, + NamedKey::PowerOff => Named::PowerOff, + NamedKey::PrintScreen => Named::PrintScreen, + NamedKey::Hibernate => Named::Hibernate, + NamedKey::Standby => Named::Standby, + NamedKey::WakeUp => Named::WakeUp, + NamedKey::AllCandidates => Named::AllCandidates, + NamedKey::Alphanumeric => Named::Alphanumeric, + NamedKey::CodeInput => Named::CodeInput, + NamedKey::Compose => Named::Compose, + NamedKey::Convert => Named::Convert, + NamedKey::FinalMode => Named::FinalMode, + NamedKey::GroupFirst => Named::GroupFirst, + NamedKey::GroupLast => Named::GroupLast, + NamedKey::GroupNext => Named::GroupNext, + NamedKey::GroupPrevious => Named::GroupPrevious, + NamedKey::ModeChange => Named::ModeChange, + NamedKey::NextCandidate => Named::NextCandidate, + NamedKey::NonConvert => Named::NonConvert, + NamedKey::PreviousCandidate => Named::PreviousCandidate, + NamedKey::Process => Named::Process, + NamedKey::SingleCandidate => Named::SingleCandidate, + NamedKey::HangulMode => Named::HangulMode, + NamedKey::HanjaMode => Named::HanjaMode, + NamedKey::JunjaMode => Named::JunjaMode, + NamedKey::Eisu => Named::Eisu, + NamedKey::Hankaku => Named::Hankaku, + NamedKey::Hiragana => Named::Hiragana, + NamedKey::HiraganaKatakana => Named::HiraganaKatakana, + NamedKey::KanaMode => Named::KanaMode, + NamedKey::KanjiMode => Named::KanjiMode, + NamedKey::Katakana => Named::Katakana, + NamedKey::Romaji => Named::Romaji, + NamedKey::Zenkaku => Named::Zenkaku, + NamedKey::ZenkakuHankaku => Named::ZenkakuHankaku, + NamedKey::Soft1 => Named::Soft1, + NamedKey::Soft2 => Named::Soft2, + NamedKey::Soft3 => Named::Soft3, + NamedKey::Soft4 => Named::Soft4, + NamedKey::ChannelDown => Named::ChannelDown, + NamedKey::ChannelUp => Named::ChannelUp, + NamedKey::Close => Named::Close, + NamedKey::MailForward => Named::MailForward, + NamedKey::MailReply => Named::MailReply, + NamedKey::MailSend => Named::MailSend, + NamedKey::MediaClose => Named::MediaClose, + NamedKey::MediaFastForward => Named::MediaFastForward, + NamedKey::MediaPause => Named::MediaPause, + NamedKey::MediaPlay => Named::MediaPlay, + NamedKey::MediaPlayPause => Named::MediaPlayPause, + NamedKey::MediaRecord => Named::MediaRecord, + NamedKey::MediaRewind => Named::MediaRewind, + NamedKey::MediaStop => Named::MediaStop, + NamedKey::MediaTrackNext => Named::MediaTrackNext, + NamedKey::MediaTrackPrevious => Named::MediaTrackPrevious, + NamedKey::New => Named::New, + NamedKey::Open => Named::Open, + NamedKey::Print => Named::Print, + NamedKey::Save => Named::Save, + NamedKey::SpellCheck => Named::SpellCheck, + NamedKey::Key11 => Named::Key11, + NamedKey::Key12 => Named::Key12, + NamedKey::AudioBalanceLeft => Named::AudioBalanceLeft, + NamedKey::AudioBalanceRight => Named::AudioBalanceRight, + NamedKey::AudioBassBoostDown => Named::AudioBassBoostDown, + NamedKey::AudioBassBoostToggle => Named::AudioBassBoostToggle, + NamedKey::AudioBassBoostUp => Named::AudioBassBoostUp, + NamedKey::AudioFaderFront => Named::AudioFaderFront, + NamedKey::AudioFaderRear => Named::AudioFaderRear, + NamedKey::AudioSurroundModeNext => Named::AudioSurroundModeNext, + NamedKey::AudioTrebleDown => Named::AudioTrebleDown, + NamedKey::AudioTrebleUp => Named::AudioTrebleUp, + NamedKey::AudioVolumeDown => Named::AudioVolumeDown, + NamedKey::AudioVolumeUp => Named::AudioVolumeUp, + NamedKey::AudioVolumeMute => Named::AudioVolumeMute, + NamedKey::MicrophoneToggle => Named::MicrophoneToggle, + NamedKey::MicrophoneVolumeDown => Named::MicrophoneVolumeDown, + NamedKey::MicrophoneVolumeUp => Named::MicrophoneVolumeUp, + NamedKey::MicrophoneVolumeMute => Named::MicrophoneVolumeMute, + NamedKey::SpeechCorrectionList => Named::SpeechCorrectionList, + NamedKey::SpeechInputToggle => Named::SpeechInputToggle, + NamedKey::LaunchApplication1 => Named::LaunchApplication1, + NamedKey::LaunchApplication2 => Named::LaunchApplication2, + NamedKey::LaunchCalendar => Named::LaunchCalendar, + NamedKey::LaunchContacts => Named::LaunchContacts, + NamedKey::LaunchMail => Named::LaunchMail, + NamedKey::LaunchMediaPlayer => Named::LaunchMediaPlayer, + NamedKey::LaunchMusicPlayer => Named::LaunchMusicPlayer, + NamedKey::LaunchPhone => Named::LaunchPhone, + NamedKey::LaunchScreenSaver => Named::LaunchScreenSaver, + NamedKey::LaunchSpreadsheet => Named::LaunchSpreadsheet, + NamedKey::LaunchWebBrowser => Named::LaunchWebBrowser, + NamedKey::LaunchWebCam => Named::LaunchWebCam, + NamedKey::LaunchWordProcessor => Named::LaunchWordProcessor, + NamedKey::BrowserBack => Named::BrowserBack, + NamedKey::BrowserFavorites => Named::BrowserFavorites, + NamedKey::BrowserForward => Named::BrowserForward, + NamedKey::BrowserHome => Named::BrowserHome, + NamedKey::BrowserRefresh => Named::BrowserRefresh, + NamedKey::BrowserSearch => Named::BrowserSearch, + NamedKey::BrowserStop => Named::BrowserStop, + NamedKey::AppSwitch => Named::AppSwitch, + NamedKey::Call => Named::Call, + NamedKey::Camera => Named::Camera, + NamedKey::CameraFocus => Named::CameraFocus, + NamedKey::EndCall => Named::EndCall, + NamedKey::GoBack => Named::GoBack, + NamedKey::GoHome => Named::GoHome, + NamedKey::HeadsetHook => Named::HeadsetHook, + NamedKey::LastNumberRedial => Named::LastNumberRedial, + NamedKey::Notification => Named::Notification, + NamedKey::MannerMode => Named::MannerMode, + NamedKey::VoiceDial => Named::VoiceDial, + NamedKey::TV => Named::TV, + NamedKey::TV3DMode => Named::TV3DMode, + NamedKey::TVAntennaCable => Named::TVAntennaCable, + NamedKey::TVAudioDescription => Named::TVAudioDescription, + NamedKey::TVAudioDescriptionMixDown => { + Named::TVAudioDescriptionMixDown + } + NamedKey::TVAudioDescriptionMixUp => { + Named::TVAudioDescriptionMixUp + } + NamedKey::TVContentsMenu => Named::TVContentsMenu, + NamedKey::TVDataService => Named::TVDataService, + NamedKey::TVInput => Named::TVInput, + NamedKey::TVInputComponent1 => Named::TVInputComponent1, + NamedKey::TVInputComponent2 => Named::TVInputComponent2, + NamedKey::TVInputComposite1 => Named::TVInputComposite1, + NamedKey::TVInputComposite2 => Named::TVInputComposite2, + NamedKey::TVInputHDMI1 => Named::TVInputHDMI1, + NamedKey::TVInputHDMI2 => Named::TVInputHDMI2, + NamedKey::TVInputHDMI3 => Named::TVInputHDMI3, + NamedKey::TVInputHDMI4 => Named::TVInputHDMI4, + NamedKey::TVInputVGA1 => Named::TVInputVGA1, + NamedKey::TVMediaContext => Named::TVMediaContext, + NamedKey::TVNetwork => Named::TVNetwork, + NamedKey::TVNumberEntry => Named::TVNumberEntry, + NamedKey::TVPower => Named::TVPower, + NamedKey::TVRadioService => Named::TVRadioService, + NamedKey::TVSatellite => Named::TVSatellite, + NamedKey::TVSatelliteBS => Named::TVSatelliteBS, + NamedKey::TVSatelliteCS => Named::TVSatelliteCS, + NamedKey::TVSatelliteToggle => Named::TVSatelliteToggle, + NamedKey::TVTerrestrialAnalog => Named::TVTerrestrialAnalog, + NamedKey::TVTerrestrialDigital => Named::TVTerrestrialDigital, + NamedKey::TVTimer => Named::TVTimer, + NamedKey::AVRInput => Named::AVRInput, + NamedKey::AVRPower => Named::AVRPower, + NamedKey::ColorF0Red => Named::ColorF0Red, + NamedKey::ColorF1Green => Named::ColorF1Green, + NamedKey::ColorF2Yellow => Named::ColorF2Yellow, + NamedKey::ColorF3Blue => Named::ColorF3Blue, + NamedKey::ColorF4Grey => Named::ColorF4Grey, + NamedKey::ColorF5Brown => Named::ColorF5Brown, + NamedKey::ClosedCaptionToggle => Named::ClosedCaptionToggle, + NamedKey::Dimmer => Named::Dimmer, + NamedKey::DisplaySwap => Named::DisplaySwap, + NamedKey::DVR => Named::DVR, + NamedKey::Exit => Named::Exit, + NamedKey::FavoriteClear0 => Named::FavoriteClear0, + NamedKey::FavoriteClear1 => Named::FavoriteClear1, + NamedKey::FavoriteClear2 => Named::FavoriteClear2, + NamedKey::FavoriteClear3 => Named::FavoriteClear3, + NamedKey::FavoriteRecall0 => Named::FavoriteRecall0, + NamedKey::FavoriteRecall1 => Named::FavoriteRecall1, + NamedKey::FavoriteRecall2 => Named::FavoriteRecall2, + NamedKey::FavoriteRecall3 => Named::FavoriteRecall3, + NamedKey::FavoriteStore0 => Named::FavoriteStore0, + NamedKey::FavoriteStore1 => Named::FavoriteStore1, + NamedKey::FavoriteStore2 => Named::FavoriteStore2, + NamedKey::FavoriteStore3 => Named::FavoriteStore3, + NamedKey::Guide => Named::Guide, + NamedKey::GuideNextDay => Named::GuideNextDay, + NamedKey::GuidePreviousDay => Named::GuidePreviousDay, + NamedKey::Info => Named::Info, + NamedKey::InstantReplay => Named::InstantReplay, + NamedKey::Link => Named::Link, + NamedKey::ListProgram => Named::ListProgram, + NamedKey::LiveContent => Named::LiveContent, + NamedKey::Lock => Named::Lock, + NamedKey::MediaApps => Named::MediaApps, + NamedKey::MediaAudioTrack => Named::MediaAudioTrack, + NamedKey::MediaLast => Named::MediaLast, + NamedKey::MediaSkipBackward => Named::MediaSkipBackward, + NamedKey::MediaSkipForward => Named::MediaSkipForward, + NamedKey::MediaStepBackward => Named::MediaStepBackward, + NamedKey::MediaStepForward => Named::MediaStepForward, + NamedKey::MediaTopMenu => Named::MediaTopMenu, + NamedKey::NavigateIn => Named::NavigateIn, + NamedKey::NavigateNext => Named::NavigateNext, + NamedKey::NavigateOut => Named::NavigateOut, + NamedKey::NavigatePrevious => Named::NavigatePrevious, + NamedKey::NextFavoriteChannel => Named::NextFavoriteChannel, + NamedKey::NextUserProfile => Named::NextUserProfile, + NamedKey::OnDemand => Named::OnDemand, + NamedKey::Pairing => Named::Pairing, + NamedKey::PinPDown => Named::PinPDown, + NamedKey::PinPMove => Named::PinPMove, + NamedKey::PinPToggle => Named::PinPToggle, + NamedKey::PinPUp => Named::PinPUp, + NamedKey::PlaySpeedDown => Named::PlaySpeedDown, + NamedKey::PlaySpeedReset => Named::PlaySpeedReset, + NamedKey::PlaySpeedUp => Named::PlaySpeedUp, + NamedKey::RandomToggle => Named::RandomToggle, + NamedKey::RcLowBattery => Named::RcLowBattery, + NamedKey::RecordSpeedNext => Named::RecordSpeedNext, + NamedKey::RfBypass => Named::RfBypass, + NamedKey::ScanChannelsToggle => Named::ScanChannelsToggle, + NamedKey::ScreenModeNext => Named::ScreenModeNext, + NamedKey::Settings => Named::Settings, + NamedKey::SplitScreenToggle => Named::SplitScreenToggle, + NamedKey::STBInput => Named::STBInput, + NamedKey::STBPower => Named::STBPower, + NamedKey::Subtitle => Named::Subtitle, + NamedKey::Teletext => Named::Teletext, + NamedKey::VideoModeNext => Named::VideoModeNext, + NamedKey::Wink => Named::Wink, + NamedKey::ZoomToggle => Named::ZoomToggle, + NamedKey::F1 => Named::F1, + NamedKey::F2 => Named::F2, + NamedKey::F3 => Named::F3, + NamedKey::F4 => Named::F4, + NamedKey::F5 => Named::F5, + NamedKey::F6 => Named::F6, + NamedKey::F7 => Named::F7, + NamedKey::F8 => Named::F8, + NamedKey::F9 => Named::F9, + NamedKey::F10 => Named::F10, + NamedKey::F11 => Named::F11, + NamedKey::F12 => Named::F12, + NamedKey::F13 => Named::F13, + NamedKey::F14 => Named::F14, + NamedKey::F15 => Named::F15, + NamedKey::F16 => Named::F16, + NamedKey::F17 => Named::F17, + NamedKey::F18 => Named::F18, + NamedKey::F19 => Named::F19, + NamedKey::F20 => Named::F20, + NamedKey::F21 => Named::F21, + NamedKey::F22 => Named::F22, + NamedKey::F23 => Named::F23, + NamedKey::F24 => Named::F24, + NamedKey::F25 => Named::F25, + NamedKey::F26 => Named::F26, + NamedKey::F27 => Named::F27, + NamedKey::F28 => Named::F28, + NamedKey::F29 => Named::F29, + NamedKey::F30 => Named::F30, + NamedKey::F31 => Named::F31, + NamedKey::F32 => Named::F32, + NamedKey::F33 => Named::F33, + NamedKey::F34 => Named::F34, + NamedKey::F35 => Named::F35, + _ => return keyboard::Key::Unidentified, + }) } - winit::event::VirtualKeyCode::NextTrack => KeyCode::NextTrack, - winit::event::VirtualKeyCode::NoConvert => KeyCode::NoConvert, - winit::event::VirtualKeyCode::NumpadComma => KeyCode::NumpadComma, - winit::event::VirtualKeyCode::NumpadEnter => KeyCode::NumpadEnter, - winit::event::VirtualKeyCode::NumpadEquals => KeyCode::NumpadEquals, - winit::event::VirtualKeyCode::OEM102 => KeyCode::OEM102, - winit::event::VirtualKeyCode::Period => KeyCode::Period, - winit::event::VirtualKeyCode::PlayPause => KeyCode::PlayPause, - winit::event::VirtualKeyCode::Power => KeyCode::Power, - winit::event::VirtualKeyCode::PrevTrack => KeyCode::PrevTrack, - winit::event::VirtualKeyCode::RAlt => KeyCode::RAlt, - winit::event::VirtualKeyCode::RBracket => KeyCode::RBracket, - winit::event::VirtualKeyCode::RControl => KeyCode::RControl, - winit::event::VirtualKeyCode::RShift => KeyCode::RShift, - winit::event::VirtualKeyCode::RWin => KeyCode::RWin, - winit::event::VirtualKeyCode::Semicolon => KeyCode::Semicolon, - winit::event::VirtualKeyCode::Slash => KeyCode::Slash, - winit::event::VirtualKeyCode::Sleep => KeyCode::Sleep, - winit::event::VirtualKeyCode::Stop => KeyCode::Stop, - winit::event::VirtualKeyCode::NumpadSubtract => KeyCode::NumpadSubtract, - winit::event::VirtualKeyCode::Sysrq => KeyCode::Sysrq, - winit::event::VirtualKeyCode::Tab => KeyCode::Tab, - winit::event::VirtualKeyCode::Underline => KeyCode::Underline, - winit::event::VirtualKeyCode::Unlabeled => KeyCode::Unlabeled, - winit::event::VirtualKeyCode::VolumeDown => KeyCode::VolumeDown, - winit::event::VirtualKeyCode::VolumeUp => KeyCode::VolumeUp, - winit::event::VirtualKeyCode::Wake => KeyCode::Wake, - winit::event::VirtualKeyCode::WebBack => KeyCode::WebBack, - winit::event::VirtualKeyCode::WebFavorites => KeyCode::WebFavorites, - winit::event::VirtualKeyCode::WebForward => KeyCode::WebForward, - winit::event::VirtualKeyCode::WebHome => KeyCode::WebHome, - winit::event::VirtualKeyCode::WebRefresh => KeyCode::WebRefresh, - winit::event::VirtualKeyCode::WebSearch => KeyCode::WebSearch, - winit::event::VirtualKeyCode::WebStop => KeyCode::WebStop, - winit::event::VirtualKeyCode::Yen => KeyCode::Yen, - winit::event::VirtualKeyCode::Copy => KeyCode::Copy, - winit::event::VirtualKeyCode::Paste => KeyCode::Paste, - winit::event::VirtualKeyCode::Cut => KeyCode::Cut, - winit::event::VirtualKeyCode::Asterisk => KeyCode::Asterisk, + _ => keyboard::Key::Unidentified, } } @@ -529,13 +812,3 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } - -// As defined in: http://www.unicode.org/faq/private_use.html -pub(crate) fn is_private_use_character(c: char) -> bool { - matches!( - c, - '\u{E000}'..='\u{F8FF}' - | '\u{F0000}'..='\u{FFFFD}' - | '\u{100000}'..='\u{10FFFD}' - ) -} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 95b55bb9..948576a2 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -33,6 +33,9 @@ pub use iced_runtime::futures; pub use iced_style as style; pub use winit; +#[cfg(feature = "multi-window")] +pub mod multi_window; + #[cfg(feature = "application")] pub mod application; pub mod clipboard; @@ -43,17 +46,11 @@ pub mod settings; pub mod system; mod error; -mod position; mod proxy; #[cfg(feature = "application")] pub use application::Application; -#[cfg(feature = "trace")] -pub use application::Profiler; pub use clipboard::Clipboard; pub use error::Error; -pub use position::Position; pub use proxy::Proxy; pub use settings::Settings; - -pub use iced_graphics::Viewport; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs new file mode 100644 index 00000000..3f0ba056 --- /dev/null +++ b/winit/src/multi_window.rs @@ -0,0 +1,1189 @@ +//! Create interactive, native cross-platform applications for WGPU. +mod state; +mod window_manager; + +pub use state::State; + +use crate::conversion; +use crate::core; +use crate::core::renderer; +use crate::core::widget::operation; +use crate::core::window; +use crate::core::Size; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::{task, Future, StreamExt}; +use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics::{compositor, Compositor}; +use crate::multi_window::window_manager::WindowManager; +use crate::runtime::command::{self, Command}; +use crate::runtime::multi_window::Program; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::Debug; +use crate::style::application::StyleSheet; +use crate::{Clipboard, Error, Proxy, Settings}; + +use std::collections::HashMap; +use std::mem::ManuallyDrop; +use std::sync::Arc; +use std::time::Instant; + +/// An interactive, native, cross-platform, multi-windowed application. +/// +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Application: Program +where + <Self::Renderer as core::Renderer>::Theme: StyleSheet, +{ + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Returns the current `Theme` of the [`Application`]. + fn theme( + &self, + window: window::Id, + ) -> <Self::Renderer as core::Renderer>::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style( + &self, + ) -> <<Self::Renderer as core::Renderer>::Theme as StyleSheet>::Style { + Default::default() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } +} + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use winit::event_loop::EventLoopBuilder; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoopBuilder::with_user_event() + .build() + .expect("Create event loop"); + + let proxy = event_loop.create_proxy(); + + let runtime = { + let proxy = Proxy::new(event_loop.create_proxy()); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + + Runtime::new(executor, proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let should_main_be_visible = settings.window.visible; + let exit_on_close_request = settings.window.exit_on_close_request; + + let builder = conversion::window_settings( + settings.window, + &application.title(window::Id::MAIN), + event_loop.primary_monitor(), + settings.id, + ) + .with_visible(false); + + log::info!("Window builder: {:#?}", builder); + + let main_window = Arc::new( + builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?, + ); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = main_window.canvas(); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!("#{}", target)) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1(&canvas) + .expect(&format!("Could not replace #{}", node.id())); + } + None => { + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } + }; + } + + let mut compositor = C::new(compositor_settings, main_window.clone())?; + + let mut window_manager = WindowManager::new(); + let _ = window_manager.insert( + window::Id::MAIN, + main_window, + &application, + &mut compositor, + exit_on_close_request, + ); + + let (mut event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window_manager, + should_main_be_visible, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + let _ = event_loop.run(move |event, event_loop| { + if event_loop.exiting() { + return; + } + + event_sender + .start_send(Event::EventLoopAwakened(event)) + .expect("Send event"); + + loop { + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => match control_receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = conversion::window_settings( + settings, &title, monitor, None, + ) + .build(event_loop) + .expect("Failed to build window"); + + event_sender + .start_send(Event::WindowCreated { + id, + window, + exit_on_close_request, + }) + .expect("Send event"); + } + Control::Exit => { + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + }); + + Ok(()) +} + +enum Event<Message: 'static> { + WindowCreated { + id: window::Id, + window: winit::window::Window, + exit_on_close_request: bool, + }, + EventLoopAwakened(winit::event::Event<Message>), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + }, +} + +async fn run_instance<A, E, C>( + mut application: A, + mut compositor: C, + mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut proxy: winit::event_loop::EventLoopProxy<A::Message>, + mut debug: Debug, + mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, + mut control_sender: mpsc::UnboundedSender<Control>, + init_command: Command<A::Message>, + mut window_manager: WindowManager<A, C>, + should_main_window_be_visible: bool, +) where + A: Application + 'static, + E: Executor + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + let main_window = window_manager + .get_mut(window::Id::MAIN) + .expect("Get main window"); + + if should_main_window_be_visible { + main_window.raw.set_visible(true); + } + + let mut clipboard = Clipboard::connect(&main_window.raw); + let mut events = { + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Opened { + position: main_window.position(), + size: main_window.size(), + }, + ), + )] + }; + + let mut ui_caches = HashMap::new(); + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + HashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), + )); + + run_command( + &application, + &mut compositor, + init_command, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut window_manager, + &mut ui_caches, + ); + + runtime.track(application.subscription().into_recipes()); + + let mut messages = Vec::new(); + + debug.startup_finished(); + + 'main: while let Some(event) = event_receiver.next().await { + match event { + Event::WindowCreated { + id, + window, + exit_on_close_request, + } => { + let window = window_manager.insert( + id, + Arc::new(window), + &application, + &mut compositor, + exit_on_close_request, + ); + + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &application, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + &mut debug, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); + + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Opened { + position: window.position(), + size: window.size(), + }, + ), + )); + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents( + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. }, + ) => { + for (_id, window) in window_manager.iter_mut() { + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + use crate::core::event; + + events.push(( + None, + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), + )); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::WindowEvent { + window_id: id, + event: event::WindowEvent::RedrawRequested, + .. + } => { + let Some((id, window)) = + window_manager.get_mut_alias(id) + else { + continue; + }; + + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + debug.draw_started(); + let new_mouse_interaction = ui.draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + cursor, + ); + debug.draw_finished(); + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = new_mouse_interaction; + } + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send(Control::ChangeFlow( + match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + window.raw.request_redraw(); + + ControlFlow::Wait + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }, + )); + + let physical_size = window.state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } + + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); + + debug.layout_started(); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + debug.layout_finished(); + + debug.draw_started(); + let new_mouse_interaction = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + window.state.cursor(), + ); + debug.draw_finished(); + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + debug.render_started(); + match compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ) { + Ok(()) => { + debug.render_finished(); + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + debug.render_finished(); + + log::error!( + "Error {error:?} when \ + presenting surface." + ); + + // Try rendering all windows again next frame. + for (_id, window) in + window_manager.iter_mut() + { + window.raw.request_redraw(); + } + } + }, + } + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + let _ = window_manager.remove(id); + let _ = user_interfaces.remove(&id); + let _ = ui_caches.remove(&id); + + events.push(( + None, + core::Event::Window(id, window::Event::Closed), + )); + + if window_manager.is_empty() { + break 'main; + } + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + id, + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); + } + } + } + event::Event::AboutToWait => { + if events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(id) || window_id.is_none() + { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if window_events.is_empty() && messages.is_empty() { + continue; + } + + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + window.raw.request_redraw(); + + if !uis_stale { + uis_stale = matches!( + ui_state, + user_interface::State::Outdated + ); + } + + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + } + + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: HashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut messages, + &mut window_manager, + &mut cached_interfaces, + ); + + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &application, + id, + &window.raw, + ); + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } + + // rebuild UIs with the synchronized states + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + cached_interfaces, + )); + } + } + _ => {} + } + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Application`]. +fn build_user_interface<'a, A: Application>( + application: &'a A, + cache: user_interface::Cache, + renderer: &mut A::Renderer, + size: Size, + debug: &mut Debug, + id: window::Id, +) -> UserInterface<'a, A::Message, A::Renderer> +where + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + debug.view_started(); + let view = application.view(id); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} + +/// Updates a multi-window [`Application`] by feeding it messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`]. +fn update<A: Application, C, E: Executor>( + application: &mut A, + compositor: &mut C, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, + messages: &mut Vec<A::Message>, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, +) where + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + for message in messages.drain(..) { + debug.log_message(&message); + debug.update_started(); + + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + compositor, + command, + runtime, + clipboard, + control_sender, + proxy, + debug, + window_manager, + ui_caches, + ); + } + + let subscription = application.subscription(); + runtime.track(subscription.into_recipes()); +} + +/// Runs the actions of a [`Command`]. +fn run_command<A, C, E>( + application: &A, + compositor: &mut C, + command: Command<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, +) where + A: Application, + E: Executor, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + for action in command.actions() { + match action { + command::Action::Future(future) => { + runtime.spawn(Box::pin(future)); + } + command::Action::Stream(stream) => { + runtime.run(Box::pin(stream)); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(tag) => { + let message = tag(clipboard.read()); + + proxy + .send_event(message) + .expect("Send message to event loop"); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents); + } + }, + command::Action::Window(action) => match action { + window::Action::Spawn(id, settings) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send control action"); + } + window::Action::Close(id) => { + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); + + if window_manager.is_empty() { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + } + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + } + window::Action::FetchSize(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + proxy + .send_event(callback(Size::new( + size.width, + size.height, + ))) + .expect("Send message to event loop"); + } + } + window::Action::FetchMaximized(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(callback(window.raw.is_maximized())) + .expect("Send message to event loop"); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::FetchMinimized(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(callback(window.raw.is_minimized())) + .expect("Send message to event loop"); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); + } + } + window::Action::ChangeMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } + } + window::Action::ChangeIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::FetchMode(id, tag) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + proxy + .send_event(tag(mode)) + .expect("Event loop doesn't exist."); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::FetchId(id, tag) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(tag(window.raw.id().into())) + .expect("Event loop doesn't exist."); + } + } + window::Action::Screenshot(id, tag) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); + + proxy + .send_event(tag(window::Screenshot::new( + bytes, + window.state.physical_size(), + ))) + .expect("Event loop doesn't exist."); + } + } + }, + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); + let proxy = proxy.clone(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let message = _tag(information); + + proxy + .send_event(message) + .expect("Event loop doesn't exist."); + }); + } + } + }, + command::Action::Widget(action) => { + let mut current_operation = Some(action); + + let mut uis = build_user_interfaces( + application, + debug, + window_manager, + std::mem::take(ui_caches), + ); + + 'operate: while let Some(mut operation) = + current_operation.take() + { + for (id, ui) in uis.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Event loop doesn't exist."); + + // operation completed, don't need to try to operate on rest of UIs + break 'operate; + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + } + + *ui_caches = + uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); + } + command::Action::LoadFont { bytes, tagger } => { + use crate::core::text::Renderer; + + // TODO change this once we change each renderer to having a single backend reference.. :pain: + // TODO: Error handling (?) + for (_, window) in window_manager.iter_mut() { + window.renderer.load_font(bytes.clone()); + } + + proxy + .send_event(tagger(Ok(()))) + .expect("Send message to event loop"); + } + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, A: Application, C: Compositor>( + application: &'a A, + debug: &mut Debug, + window_manager: &mut WindowManager<A, C>, + mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, +) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Renderer>> +where + <A::Renderer as core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + cached_user_interfaces + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + + Some(( + id, + build_user_interface( + application, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, + id, + ), + )) + }) + .collect() +} + +/// Returns true if the provided event should cause an [`Application`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if c == "q" && _modifiers.super_key() => true, + _ => false, + } +} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs new file mode 100644 index 00000000..235771f4 --- /dev/null +++ b/winit/src/multi_window/state.rs @@ -0,0 +1,242 @@ +use crate::conversion; +use crate::core; +use crate::core::{mouse, window}; +use crate::core::{Color, Size}; +use crate::graphics::Viewport; +use crate::multi_window::Application; +use crate::style::application; +use std::fmt::{Debug, Formatter}; + +use iced_style::application::StyleSheet; +use winit::event::{Touch, WindowEvent}; +use winit::window::Window; + +/// The state of a multi-windowed [`Application`]. +pub struct State<A: Application> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: u64, + cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, + modifiers: winit::keyboard::ModifiersState, + theme: <A::Renderer as core::Renderer>::Theme, + appearance: application::Appearance, +} + +impl<A: Application> Debug for State<A> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("multi_window::State") + .field("title", &self.title) + .field("scale_factor", &self.scale_factor) + .field("viewport", &self.viewport) + .field("viewport_version", &self.viewport_version) + .field("cursor_position", &self.cursor_position) + .field("appearance", &self.appearance) + .finish() + } +} + +impl<A: Application> State<A> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`]'s `window`. + pub fn new( + application: &A, + window_id: window::Id, + window: &Window, + ) -> Self { + let title = application.title(window_id); + let scale_factor = application.scale_factor(window_id); + let theme = application.theme(window_id); + let appearance = theme.appearance(&application.style()); + + let viewport = { + let physical_size = window.inner_size(); + + Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor() * scale_factor, + ) + }; + + Self { + title, + scale_factor, + viewport, + viewport_version: 0, + cursor_position: None, + modifiers: winit::keyboard::ModifiersState::default(), + theme, + appearance, + } + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// Returns the version of the [`Viewport`] of the [`State`]. + /// + /// The version is incremented every time the [`Viewport`] changes. + pub fn viewport_version(&self) -> u64 { + self.viewport_version + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size<u32> { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size<f32> { + self.viewport.logical_size() + } + + /// Returns the current scale factor of the [`Viewport`] of the [`State`]. + pub fn scale_factor(&self) -> f64 { + self.viewport.scale_factor() + } + + /// Returns the current cursor position of the [`State`]. + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + conversion::cursor_position( + cursor_position, + self.viewport.scale_factor(), + ) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> winit::keyboard::ModifiersState { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme { + &self.theme + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.appearance.background_color + } + + /// Returns the current text [`Color`] of the [`State`]. + pub fn text_color(&self) -> Color { + self.appearance.text_color + } + + /// Processes the provided window event and updates the [`State`] accordingly. + pub fn update( + &mut self, + window: &Window, + event: &WindowEvent, + _debug: &mut crate::runtime::Debug, + ) { + match event { + WindowEvent::Resized(new_size) => { + let size = Size::new(new_size.width, new_size.height); + + self.viewport = Viewport::with_physical_size( + size, + window.scale_factor() * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + .. + } => { + let size = self.viewport.physical_size(); + + self.viewport = Viewport::with_physical_size( + size, + new_scale_factor * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::CursorMoved { position, .. } + | WindowEvent::Touch(Touch { + location: position, .. + }) => { + self.cursor_position = Some(*position); + } + WindowEvent::CursorLeft { .. } => { + self.cursor_position = None; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + self.modifiers = new_modifiers.state(); + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } + } + + /// Synchronizes the [`State`] with its [`Application`] and its respective + /// window. + /// + /// Normally, an [`Application`] should be synchronized with its [`State`] + /// and window after calling [`State::update`]. + pub fn synchronize( + &mut self, + application: &A, + window_id: window::Id, + window: &Window, + ) { + // Update window title + let new_title = application.title(window_id); + + if self.title != new_title { + window.set_title(&new_title); + self.title = new_title; + } + + // Update scale factor and size + let new_scale_factor = application.scale_factor(window_id); + let new_size = window.inner_size(); + let current_size = self.viewport.physical_size(); + + if self.scale_factor != new_scale_factor + || (current_size.width, current_size.height) + != (new_size.width, new_size.height) + { + self.viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor() * new_scale_factor, + ); + self.viewport_version = self.viewport_version.wrapping_add(1); + + self.scale_factor = new_scale_factor; + } + + // Update theme and appearance + self.theme = application.theme(window_id); + self.appearance = self.theme.appearance(&application.style()); + } +} diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs new file mode 100644 index 00000000..9e15f9ea --- /dev/null +++ b/winit/src/multi_window/window_manager.rs @@ -0,0 +1,157 @@ +use crate::core::mouse; +use crate::core::window::Id; +use crate::core::{Point, Size}; +use crate::graphics::Compositor; +use crate::multi_window::{Application, State}; +use crate::style::application::StyleSheet; + +use std::collections::BTreeMap; +use std::sync::Arc; +use winit::monitor::MonitorHandle; + +#[allow(missing_debug_implementations)] +pub struct WindowManager<A: Application, C: Compositor> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + aliases: BTreeMap<winit::window::WindowId, Id>, + entries: BTreeMap<Id, Window<A, C>>, +} + +impl<A, C> WindowManager<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub fn new() -> Self { + Self { + aliases: BTreeMap::new(), + entries: BTreeMap::new(), + } + } + + pub fn insert( + &mut self, + id: Id, + window: Arc<winit::window::Window>, + application: &A, + compositor: &mut C, + exit_on_close_request: bool, + ) -> &mut Window<A, C> { + let state = State::new(application, id, &window); + let viewport_version = state.viewport_version(); + let physical_size = state.physical_size(); + let surface = compositor.create_surface( + window.clone(), + physical_size.width, + physical_size.height, + ); + let renderer = compositor.create_renderer(); + + let _ = self.aliases.insert(window.id(), id); + + let _ = self.entries.insert( + id, + Window { + raw: window, + state, + viewport_version, + exit_on_close_request, + surface, + renderer, + mouse_interaction: mouse::Interaction::Idle, + }, + ); + + self.entries + .get_mut(&id) + .expect("Get window that was just inserted") + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn iter_mut( + &mut self, + ) -> impl Iterator<Item = (Id, &mut Window<A, C>)> { + self.entries.iter_mut().map(|(k, v)| (*k, v)) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> { + self.entries.get_mut(&id) + } + + pub fn get_mut_alias( + &mut self, + id: winit::window::WindowId, + ) -> Option<(Id, &mut Window<A, C>)> { + let id = self.aliases.get(&id).copied()?; + + Some((id, self.get_mut(id)?)) + } + + pub fn last_monitor(&self) -> Option<MonitorHandle> { + self.entries.values().last()?.raw.current_monitor() + } + + pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> { + let window = self.entries.remove(&id)?; + let _ = self.aliases.remove(&window.raw.id()); + + Some(window) + } +} + +impl<A, C> Default for WindowManager<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_debug_implementations)] +pub struct Window<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub raw: Arc<winit::window::Window>, + pub state: State<A>, + pub viewport_version: u64, + pub exit_on_close_request: bool, + pub mouse_interaction: mouse::Interaction, + pub surface: C::Surface, + pub renderer: A::Renderer, +} + +impl<A, C> Window<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub fn position(&self) -> Option<Point> { + self.raw + .inner_position() + .ok() + .map(|position| position.to_logical(self.raw.scale_factor())) + .map(|position| Point { + x: position.x, + y: position.y, + }) + } + + pub fn size(&self) -> Size { + let size = self.raw.inner_size().to_logical(self.raw.scale_factor()); + + Size::new(size.width, size.height) + } +} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 867dad0f..2e541128 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,39 +1,7 @@ //! Configure your application. -#[cfg(target_os = "windows")] -#[path = "settings/windows.rs"] -mod platform; +use crate::core::window; -#[cfg(target_os = "macos")] -#[path = "settings/macos.rs"] -mod platform; - -#[cfg(target_os = "linux")] -#[path = "settings/linux.rs"] -mod platform; - -#[cfg(target_arch = "wasm32")] -#[path = "settings/wasm.rs"] -mod platform; - -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_arch = "wasm32" -)))] -#[path = "settings/other.rs"] -mod platform; - -pub use platform::PlatformSpecific; - -use crate::conversion; -use crate::core::window::{Icon, Level}; -use crate::Position; - -use winit::monitor::MonitorHandle; -use winit::window::WindowBuilder; - -use std::fmt; +use std::borrow::Cow; /// The settings of an application. #[derive(Debug, Clone, Default)] @@ -44,198 +12,14 @@ pub struct Settings<Flags> { /// communicate with it through the windowing system. pub id: Option<String>, - /// The [`Window`] settings. - pub window: Window, + /// The [`window::Settings`]. + pub window: window::Settings, /// The data needed to initialize an [`Application`]. /// /// [`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). - /// - /// [`Application`]: crate::Application - pub exit_on_close_request: bool, -} - -/// The window settings of an application. -#[derive(Clone)] -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)>, - - /// The maximum size of the window. - pub max_size: Option<(u32, u32)>, - - /// Whether the window should be visible or not. - pub visible: bool, - - /// Whether the window should be resizable or not. - pub resizable: bool, - - /// Whether the window should have a border, a title bar, etc. - pub decorations: bool, - - /// Whether the window should be transparent. - pub transparent: bool, - - /// The window [`Level`]. - pub level: Level, - - /// The window icon, which is also usually used in the taskbar - pub icon: Option<Icon>, - - /// Platform specific settings. - pub platform_specific: platform::PlatformSpecific, -} - -impl fmt::Debug for Window { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Window") - .field("size", &self.size) - .field("position", &self.position) - .field("min_size", &self.min_size) - .field("max_size", &self.max_size) - .field("visible", &self.visible) - .field("resizable", &self.resizable) - .field("decorations", &self.decorations) - .field("transparent", &self.transparent) - .field("level", &self.level) - .field("icon", &self.icon.is_some()) - .field("platform_specific", &self.platform_specific) - .finish() - } -} - -impl Window { - /// Converts the window settings into a `WindowBuilder` from `winit`. - pub fn into_builder( - self, - title: &str, - primary_monitor: Option<MonitorHandle>, - _id: Option<String>, - ) -> WindowBuilder { - let mut window_builder = WindowBuilder::new(); - - let (width, height) = self.size; - - window_builder = window_builder - .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { width, height }) - .with_resizable(self.resizable) - .with_decorations(self.decorations) - .with_transparent(self.transparent) - .with_window_icon(self.icon.and_then(conversion::icon)) - .with_window_level(conversion::window_level(self.level)) - .with_visible(self.visible); - - 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 - .with_min_inner_size(winit::dpi::LogicalSize { width, height }); - } - - if let Some((width, height)) = self.max_size { - window_builder = window_builder - .with_max_inner_size(winit::dpi::LogicalSize { width, height }); - } - - #[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do - // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. - use ::winit::platform::wayland::WindowBuilderExtWayland; - - if let Some(id) = _id { - window_builder = window_builder.with_name(id.clone(), id); - } - } - - #[cfg(target_os = "windows")] - { - use winit::platform::windows::WindowBuilderExtWindows; - #[allow(unsafe_code)] - unsafe { - window_builder = window_builder - .with_parent_window(self.platform_specific.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, - ); - } - - #[cfg(target_os = "linux")] - { - #[cfg(feature = "x11")] - { - use winit::platform::x11::WindowBuilderExtX11; - - window_builder = window_builder.with_name( - &self.platform_specific.application_id, - &self.platform_specific.application_id, - ); - } - #[cfg(feature = "wayland")] - { - use winit::platform::wayland::WindowBuilderExtWayland; - - window_builder = window_builder.with_name( - &self.platform_specific.application_id, - &self.platform_specific.application_id, - ); - } - } - - window_builder - } -} - -impl Default for Window { - fn default() -> Window { - Window { - size: (1024, 768), - position: Position::default(), - min_size: None, - max_size: None, - visible: true, - resizable: true, - decorations: true, - transparent: false, - level: Level::default(), - icon: None, - platform_specific: PlatformSpecific::default(), - } - } + /// The fonts to load on boot. + pub fonts: Vec<Cow<'static, [u8]>>, } |