diff options
293 files changed, 11294 insertions, 8578 deletions
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 9d0eb2ed..96dcc3b6 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -57,10 +57,13 @@ body: id: version attributes: label: Version - description: What version of iced are you using? + description: | + We only offer support for the `0.4` release on crates.io and the `master` branch on this repository. Which version are you using? Please make sure you are using the latest patch available (e.g. run `cargo update`). + + If you are using an older release, please upgrade to `0.4` before filing an issue. options: - master - - 0.3.0 + - 0.4 validations: required: true - type: dropdown diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 30bb3004..ba5dc190 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -8,8 +8,5 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - uses: actions/checkout@master - - name: Manually update `nix` crates # See https://github.com/nix-rust/nix/issues/1627 - run: | - cargo update --package nix:0.20.0 --precise 0.20.2 - name: Audit dependencies run: cargo audit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index beec168b..b30b7a61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,19 +16,15 @@ jobs: export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update sudo apt-get install -y libxkbcommon-dev - - name: Enable Link Time Optimizations - run: | - echo "[profile.release]" >> Cargo.toml - echo "lto = true" >> Cargo.toml - name: Build todos binary - run: cargo build --verbose --release --package todos + run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-unknown-linux-gnu - path: target/release/todos + path: target/release-opt/todos - name: Pack todos .deb package - run: cargo deb --no-build --package todos + run: cargo deb --no-build --profile release-opt --package todos - name: Rename todos .deb package run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb - name: Archive todos .deb package @@ -42,10 +38,6 @@ jobs: steps: - uses: hecrj/setup-rust-action@v1 - uses: actions/checkout@master - - name: Enable Link Time Optimizations - run: | - echo "[profile.release]" >> Cargo.toml - echo "lto = true" >> Cargo.toml - name: Enable static CRT linkage run: | mkdir .cargo @@ -55,33 +47,29 @@ jobs: run: | sed -i '1 i\#![windows_subsystem = \"windows\"]' examples/todos/src/main.rs - name: Build todos binary - run: cargo build --verbose --release --package todos + run: cargo build --verbose --profile release-opt --package todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-pc-windows-msvc - path: target/release/todos.exe + path: target/release-opt/todos.exe todos_macos: runs-on: macOS-latest steps: - uses: hecrj/setup-rust-action@v1 - uses: actions/checkout@master - - name: Enable Link Time Optimizations - run: | - echo "[profile.release]" >> Cargo.toml - echo "lto = true" >> Cargo.toml - name: Build todos binary env: MACOSX_DEPLOYMENT_TARGET: 10.14 - run: cargo build --verbose --release --package todos + run: cargo build --verbose --profile release-opt --package todos - name: Open binary via double-click - run: chmod +x target/release/todos + run: chmod +x target/release-opt/todos - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-x86_64-apple-darwin - path: target/release/todos + path: target/release-opt/todos todos_raspberry: runs-on: ubuntu-latest @@ -90,21 +78,17 @@ jobs: - uses: actions/checkout@master - name: Install cross run: cargo install cross - - name: Enable Link Time Optimizations - run: | - echo "[profile.release]" >> Cargo.toml - echo "lto = true" >> Cargo.toml - name: Build todos binary for Raspberry Pi 3/4 (64 bits) - run: cross build --verbose --release --package todos --target aarch64-unknown-linux-gnu + run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-aarch64-unknown-linux-gnu - path: target/aarch64-unknown-linux-gnu/release/todos + path: target/aarch64-unknown-linux-gnu/release-opt/todos - name: Build todos binary for Raspberry Pi 2/3/4 (32 bits) - run: cross build --verbose --release --package todos --target armv7-unknown-linux-gnueabihf + run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf - name: Archive todos binary uses: actions/upload-artifact@v1 with: name: todos-armv7-unknown-linux-gnueabihf - path: target/armv7-unknown-linux-gnueabihf/release/todos + path: target/armv7-unknown-linux-gnueabihf/release-opt/todos diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 3a8326b6..e69f4d63 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -10,11 +10,16 @@ jobs: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: hecrj/setup-rust-action@v1 + with: + rust-version: nightly - uses: actions/checkout@v2 - name: Generate documentation run: | - cargo doc --no-deps --all-features \ + RUSTDOCFLAGS="--cfg docsrs" \ + cargo doc --no-deps --all-features \ -p iced_core \ + -p iced_style \ + -p iced_futures \ -p iced_native \ -p iced_lazy \ -p iced_graphics \ diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 92caff79..42a96411 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -9,4 +9,4 @@ jobs: components: rustfmt - uses: actions/checkout@master - name: Check format - run: cargo fmt --all -- --check + run: cargo fmt --all -- --check --verbose diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..6fd98374 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,12 @@ +name: Lint +on: [push, pull_request] +jobs: + all: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + components: clippy + - uses: actions/checkout@master + - name: Check lints + run: cargo clippy --workspace --all-features --all-targets --no-deps -- -D warnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 433afadc..38b81842 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,8 @@ jobs: sudo apt-get install -y libxkbcommon-dev - name: Run tests run: | - cargo test --verbose --all - cargo test --verbose --all --all-features + cargo test --verbose --workspace + cargo test --verbose --workspace --all-features web: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index a476631d..0f0fa47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.2] - 2022-05-03 +### Fixed +- `Padding` type not exposed in `iced`. + +## [0.4.1] - 2022-05-02 +### Fixed +- Version number in `README`. + +## [0.4.0] - 2022-05-02 +### Added +- __[Stateless widgets][stateless]__ (#1284) + A brand new widget API that removes the need to keep track of internal widget state. No more `button::State` in your application! + +- __[`Component` trait][component]__ (#1131) + A new trait to implement custom widgets with internal mutable state while using composition and [The Elm Architecture]. + +- __[`Responsive` widget][responsive]__ (#1193) + A widget that is aware of its dimensions and can be used to easily build responsive user interfaces. + +- __[Experimental WebGL support][webgl]__ (#1096) + Applications can now be rendered into an HTML `canvas` when targeting Wasm by leveraging the WebGL support in [`wgpu`]. Thanks to @pacmancoder and @kaimast! + +- __[Support for Raspberry Pis and older devices][raspberry]__ (#1160) + The compatibility of our OpenGL renderer has been improved and should run on any hardware that supports OpenGL 3.0+ or OpenGL ES 2.0+. Additionally, we started maintaining [Docker images for `aarch64` and `armv7`](https://github.com/orgs/iced-rs/packages) to easily cross-compile `iced` applications and target Raspberry Pis. Thanks to @derezzedex! + +- __[Simpler `Renderer` APIs][renderer_apis]__ (#1110) + The surface of the `Renderer` APIs of the library has been considerably reduced. Instead of a `Renderer` trait per widget, now there are only 3 traits that are reused by all the widgets. + +[webgl]: https://github.com/iced-rs/iced/pull/1096 +[renderer_apis]: https://github.com/iced-rs/iced/pull/1110 +[component]: https://github.com/iced-rs/iced/pull/1131 +[raspberry]: https://github.com/iced-rs/iced/pull/1160 +[responsive]: https://github.com/iced-rs/iced/pull/1193 +[stateless]: https://github.com/iced-rs/iced/pull/1284 +[The Elm Architecture]: https://guide.elm-lang.org/architecture/ +[`wgpu`]: https://github.com/gfx-rs/wgpu + + ## [0.3.0] - 2021-03-31 ### Added - Touch support. [#57] [#650] (thanks to @simlay and @discordance!) @@ -219,7 +257,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.3.0...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.4.2...HEAD +[0.4.2]: https://github.com/iced-rs/iced/compare/0.4.1...0.4.2 +[0.4.1]: https://github.com/iced-rs/iced/compare/0.4.0...0.4.1 +[0.4.0]: https://github.com/iced-rs/iced/compare/0.3.0...0.4.0 [0.3.0]: https://github.com/iced-rs/iced/compare/0.2.0...0.3.0 [0.2.0]: https://github.com/iced-rs/iced/compare/0.1.1...0.2.0 [0.1.1]: https://github.com/iced-rs/iced/compare/0.1.0...0.1.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf6655f5..8782a2e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for considering contributing to Iced! Feel free to read [the ecosystem overview] and [the roadmap] to get an idea of the current state of the library. -The main advice for new contributors is to share your ideas with the community. Introduce yourself over our [Discord server] or [start a discussion in an issue](https://github.com/hecrj/iced/issues) explaining what you have in mind (do not be afraid of duplicated issues!). If you want to talk directly to me (@hecrj), you can also find me on Discord (`lone_scientist#9554`). +The main advice for new contributors is to share your ideas with the community. Introduce yourself over our [Discord server] or [start a discussion in an issue](https://github.com/iced-rs/iced/issues) explaining what you have in mind (do not be afraid of duplicated issues!). If you want to talk directly to me (@hecrj), you can also find me on Discord (`lone_scientist#9554`). This is a very important step. It helps to coordinate work, get on the same page, and start building trust. Please, do not skip it! Remember that [Code is the Easy Part] and also [The Hard Parts of Open Source]! @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.3.0" +version = "0.4.2" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A cross-platform GUI library inspired by Elm" @@ -14,24 +14,20 @@ resolver = "2" [features] default = ["wgpu"] -# Enables the `iced_wgpu` renderer -wgpu = ["iced_wgpu"] # Enables the `Image` widget image = ["iced_wgpu/image", "image_rs"] # Enables the `Svg` widget svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget -canvas = ["iced_wgpu/canvas"] +canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget -qr_code = ["iced_wgpu/qr_code"] +qr_code = ["iced_graphics/qr_code"] +# Enables the `iced_wgpu` renderer +wgpu = ["iced_wgpu"] # Enables using system fonts default_system_font = ["iced_wgpu/default_system_font"] # Enables the `iced_glow` renderer. Overrides `iced_wgpu` glow = ["iced_glow", "iced_glutin"] -# Enables the `Canvas` widget for `iced_glow` -glow_canvas = ["iced_glow/canvas"] -# Enables the `QRCode` widget for `iced_glow` -glow_qr_code = ["iced_glow/qr_code"] # Enables using system fonts for `iced_glow` glow_default_system_font = ["iced_glow/default_system_font"] # Enables a debug view in native platforms (press F12) @@ -44,6 +40,8 @@ async-std = ["iced_futures/async-std"] smol = ["iced_futures/smol"] # Enables advanced color conversion via `palette` palette = ["iced_core/palette"] +# Enables querying system information +system = ["iced_winit/system"] [badges] maintenance = { status = "actively-developed" } @@ -60,6 +58,7 @@ members = [ "style", "wgpu", "winit", + "examples/arc", "examples/bezier_tool", "examples/clock", "examples/color_palette", @@ -79,10 +78,12 @@ members = [ "examples/progress_bar", "examples/qr_code", "examples/scrollable", + "examples/sierpinski_triangle", "examples/solar_system", "examples/stopwatch", "examples/styling", "examples/svg", + "examples/system_information", "examples/todos", "examples/tooltip", "examples/tour", @@ -91,11 +92,13 @@ members = [ ] [dependencies] -iced_core = { version = "0.4", path = "core" } -iced_futures = { version = "0.3", path = "futures" } -iced_winit = { version = "0.3", path = "winit" } -iced_glutin = { version = "0.2", path = "glutin", optional = true } -iced_glow = { version = "0.2", path = "glow", optional = true } +iced_core = { version = "0.5", path = "core" } +iced_futures = { version = "0.4", path = "futures" } +iced_native = { version = "0.5", path = "native" } +iced_graphics = { version = "0.3", path = "graphics" } +iced_winit = { version = "0.4", path = "winit" } +iced_glutin = { version = "0.3", path = "glutin", optional = true } +iced_glow = { version = "0.3", path = "glow", optional = true } thiserror = "1.0" [dependencies.image_rs] @@ -104,11 +107,21 @@ package = "image" optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.4", path = "wgpu", optional = true } +iced_wgpu = { version = "0.5", path = "wgpu", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.4", path = "wgpu", features = ["webgl"], optional = true } +iced_wgpu = { version = "0.5", path = "wgpu", features = ["webgl"], optional = true } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] + +[profile.release-opt] +inherits = "release" +codegen-units = 1 +debug = false +lto = true +incremental = false +opt-level = 3 +overflow-checks = false +strip = "debuginfo" @@ -2,13 +2,13 @@ <img src="docs/logo.svg" width="140px" /> -# iced +# Iced [][documentation] [](https://crates.io/crates/iced) -[](https://github.com/hecrj/iced/blob/master/LICENSE) +[](https://github.com/iced-rs/iced/blob/master/LICENSE) [](https://crates.io/crates/iced) -[](https://github.com/hecrj/iced/actions) +[](https://github.com/iced-rs/iced/actions) [](https://discord.gg/3xZJ65GAhd) A cross-platform GUI library for Rust focused on simplicity and type-safety. @@ -18,33 +18,34 @@ Inspired by [Elm]. <img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="350px"> </a> <a href="https://gfycat.com/politeadorableiberianmole"> - <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> + <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif" height="350px"> </a> </div> ## Features - * Simple, easy-to-use, batteries-included API - * Type-safe, reactive programming model - * [Cross-platform support] (Windows, macOS, Linux, and [the Web]) - * Responsive layout - * Built-in widgets (including [text inputs], [scrollables], and more!) - * Custom widget support (create your own!) - * [Debug overlay with performance metrics] - * First-class support for async actions (use futures!) - * [Modular ecosystem] split into reusable parts: - * A [renderer-agnostic native runtime] enabling integration with existing systems - * Two [built-in renderers] leveraging [`wgpu`] and [`glow`] - * [`iced_wgpu`] supporting Vulkan, Metal and DX12 - * [`iced_glow`] supporting OpenGL 2.1+ and OpenGL ES 2.0+ - * A [windowing shell] - * A [web runtime] leveraging the DOM - -__iced is currently experimental software.__ [Take a look at the roadmap], + +* Simple, easy-to-use, batteries-included API +* Type-safe, reactive programming model +* [Cross-platform support] (Windows, macOS, Linux, and [the Web]) +* Responsive layout +* Built-in widgets (including [text inputs], [scrollables], and more!) +* Custom widget support (create your own!) +* [Debug overlay with performance metrics] +* First-class support for async actions (use futures!) +* [Modular ecosystem] split into reusable parts: + * A [renderer-agnostic native runtime] enabling integration with existing systems + * Two [built-in renderers] leveraging [`wgpu`] and [`glow`] + * [`iced_wgpu`] supporting Vulkan, Metal and DX12 + * [`iced_glow`] supporting OpenGL 2.1+ and OpenGL ES 2.0+ + * A [windowing shell] + * A [web runtime] leveraging the DOM + +__Iced is currently experimental software.__ [Take a look at the roadmap], [check out the issues], and [feel free to contribute!] -[Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg -[the Web]: https://iced.rs/ +[Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg +[the Web]: https://github.com/iced-rs/iced_web [text inputs]: https://gfycat.com/alertcalmcrow-rust-gui [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee @@ -63,27 +64,32 @@ __iced is currently experimental software.__ [Take a look at the roadmap], [feel free to contribute!]: #contributing--feedback ## Installation + Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.3" +iced = "0.4" ``` -__iced moves fast and the `master` branch can contain breaking changes!__ If +If your project is using a Rust edition older than 2021, then you will need to +set `resolver = "2"` in the `[package]` section as well. + +__Iced moves fast and the `master` branch can contain breaking changes!__ If you want to learn about a specific release, check out [the release list]. [the release list]: https://github.com/iced-rs/iced/releases ## Overview -Inspired by [The Elm Architecture], iced expects you to split user interfaces + +Inspired by [The Elm Architecture], Iced expects you to split user interfaces into four different concepts: - * __State__ — the state of your application - * __Messages__ — user interactions or meaningful events that you care +* __State__ — the state of your application +* __Messages__ — user interactions or meaningful events that you care about - * __View logic__ — a way to display your __state__ as widgets that +* __View logic__ — a way to display your __state__ as widgets that may produce __messages__ on user interaction - * __Update logic__ — a way to react to __messages__ and update your +* __Update logic__ — a way to react to __messages__ and update your __state__ We can build something to see how this works! Let's say we want a simple counter @@ -92,15 +98,9 @@ that can be incremented and decremented using two buttons. We start by modelling the __state__ of our application: ```rust -use iced::button; - struct Counter { // The counter value value: i32, - - // The local state of the two buttons - increment_button: button::State, - decrement_button: button::State, } ``` @@ -119,28 +119,23 @@ Now, let's show the actual counter by putting it all together in our __view logic__: ```rust -use iced::{Button, Column, Text}; +use iced::widget::{button, column, text, Column}; impl Counter { - pub fn view(&mut self) -> Column<Message> { + pub fn view(&self) -> Column<Message> { // We use a column: a simple vertical layout - Column::new() - .push( - // The increment button. We tell it to produce an - // `IncrementPressed` message when pressed - Button::new(&mut self.increment_button, Text::new("+")) - .on_press(Message::IncrementPressed), - ) - .push( - // We show the value of the counter here - Text::new(self.value.to_string()).size(50), - ) - .push( - // The decrement button. We tell it to produce a - // `DecrementPressed` message when pressed - Button::new(&mut self.decrement_button, Text::new("-")) - .on_press(Message::DecrementPressed), - ) + column![ + // The increment button. We tell it to produce an + // `IncrementPressed` message when pressed + button("+").on_press(Message::IncrementPressed), + + // We show the value of the counter here + text(self.value).size(50), + + // The decrement button. We tell it to produce a + // `DecrementPressed` message when pressed + button("-").on_press(Message::DecrementPressed), + ] } } ``` @@ -165,7 +160,7 @@ impl Counter { } ``` -And that's everything! We just wrote a whole user interface. iced is now able +And that's everything! We just wrote a whole user interface. Iced is now able to: 1. Take the result of our __view logic__ and layout its widgets. @@ -176,7 +171,8 @@ to: Browse the [documentation] and the [examples] to learn more! ## Implementation details -iced was originally born as an attempt at bringing the simplicity of [Elm] and + +Iced was originally born as an attempt at bringing the simplicity of [Elm] and [The Elm Architecture] into [Coffee], a 2D game engine I am working on. The core of the library was implemented during May 2019 in [this pull request]. @@ -189,7 +185,7 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular: <p align="center"> <a href="ECOSYSTEM.md"> - <img alt="iced ecosystem" src="docs/graphs/ecosystem.png" width="80%"> + <img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="80%"> </a> </p> @@ -200,33 +196,36 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular: [`ggez`]: https://github.com/ggez/ggez [the ecosystem]: ECOSYSTEM.md -## Common problems +## Troubleshooting + +### `GraphicsAdapterNotFound` -1. `Error: GraphicsAdapterNotFound` - - This occurs when the selected [built-in renderer] is not able to create a context. - - Often this will occur while using [`iced_wgpu`] as the renderer without - supported hardware (needs Vulkan, Metal or DX12). In this case, you could try using the - [`iced_glow`] renderer: +This occurs when the selected [built-in renderer] is not able to create a context. - First, check if it works with - ```console - $ cargo run --features "iced/glow iced/glow_canvas" --package game_of_life - ``` +Often this will occur while using [`iced_wgpu`] as the renderer without +supported hardware (needs Vulkan, Metal or DX12). In this case, you could try using the +[`iced_glow`] renderer: - and then use it in your project with - ```toml - iced = { version = "0.3", default-features = false, features = ["glow"] } - ``` +First, check if it works with - **NOTE:** Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, - but if you don't, right now there's no software fallback, so it means your hardware - doesn't support Iced. +```console +cargo run --features iced/glow --package game_of_life +``` + +and then use it in your project with + +```toml +iced = { version = "0.4", default-features = false, features = ["glow"] } +``` + +__NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, +but if you don't, right now there's no software fallback, so it means your hardware +doesn't support Iced. -[built-in renderer]: https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md#Renderers +[built-in renderer]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md#Renderers ## Contributing / Feedback + Contributions are greatly appreciated! If you want to contribute, please read our [contributing guidelines] for more details. @@ -236,15 +235,16 @@ awesome folks) over the `#games-and-graphics` and `#gui-and-ui` channels in the [Rust Community Discord]. I go by `lone_scientist#9554` there. ## Sponsors -The development of iced is sponsored by the [Cryptowatch] team at [Kraken.com] + +The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] [documentation]: https://docs.rs/iced/ -[examples]: https://github.com/hecrj/iced/tree/master/examples +[examples]: https://github.com/iced-rs/iced/tree/master/examples [Coffee]: https://github.com/hecrj/coffee [Elm]: https://elm-lang.org/ [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -[the current issues]: https://github.com/hecrj/iced/issues -[contributing guidelines]: https://github.com/hecrj/iced/blob/master/CONTRIBUTING.md +[the current issues]: https://github.com/iced-rs/iced/issues +[contributing guidelines]: https://github.com/iced-rs/iced/blob/master/CONTRIBUTING.md [Discord server]: https://discord.gg/3xZJ65GAhd [Rust Community Discord]: https://bit.ly/rust-community [Cryptowatch]: https://cryptowat.ch/charts diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..0d4e02f0 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +too-many-arguments-threshold = 20 diff --git a/core/Cargo.toml b/core/Cargo.toml index 92b8c56a..c9c7686e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.4.0" +version = "0.5.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "The essential concepts of Iced" @@ -11,7 +11,7 @@ repository = "https://github.com/iced-rs/iced" bitflags = "1.2" [dependencies.palette] -version = "0.5" +version = "0.6" optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 38777536..016761af 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -6,7 +6,7 @@ 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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Event { /// A keyboard key was pressed. KeyPressed { diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index e61f145a..bbdd8272 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -5,7 +5,7 @@ bitflags! { #[derive(Default)] pub struct Modifiers: u32{ /// The "shift" key. - const SHIFT = 0b100 << 0; + const SHIFT = 0b100; // const LSHIFT = 0b010 << 0; // const RSHIFT = 0b001 << 0; // @@ -41,21 +41,29 @@ impl Modifiers { }; /// Returns true if the [`SHIFT`] key is pressed in the [`Modifiers`]. + /// + /// [`SHIFT`]: Self::SHIFT pub fn shift(self) -> bool { self.contains(Self::SHIFT) } /// Returns true if the [`CTRL`] key is pressed in the [`Modifiers`]. + /// + /// [`CTRL`]: Self::CTRL pub fn control(self) -> bool { self.contains(Self::CTRL) } /// Returns true if the [`ALT`] key is pressed in the [`Modifiers`]. + /// + /// [`ALT`]: Self::ALT pub fn alt(self) -> bool { self.contains(Self::ALT) } /// Returns true if the [`LOGO`] key is pressed in the [`Modifiers`]. + /// + /// [`LOGO`]: Self::LOGO pub fn logo(self) -> bool { self.contains(Self::LOGO) } diff --git a/core/src/length.rs b/core/src/length.rs index 186411a5..95ea6e0e 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -1,5 +1,5 @@ /// The strategy used to fill space in a specific dimension. -#[derive(Debug, Clone, Copy, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Length { /// Fill all the remaining space Fill, diff --git a/core/src/lib.rs b/core/src/lib.rs index 3eb9f659..03ba8cca 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,13 +7,23 @@ //!  //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/master/native -//! [`iced_web`]: https://github.com/iced-rs/iced/tree/master/web -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![forbid(unsafe_code)] -#![forbid(rust_2018_idioms)] +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native +//! [`iced_web`]: https://github.com/iced-rs/iced_web +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; pub mod keyboard; pub mod mouse; diff --git a/core/src/size.rs b/core/src/size.rs index 6745c6c8..2db33a88 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -2,7 +2,7 @@ use crate::{Padding, Vector}; use std::f32; /// An amount of space in 2 dimensions. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Size<T = f32> { /// The width. pub width: T, diff --git a/core/src/vector.rs b/core/src/vector.rs index 92bb7648..b550869c 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -1,5 +1,5 @@ /// A 2D vector. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Vector<T = f32> { /// The X component of the [`Vector`] pub x: T, diff --git a/examples/README.md b/examples/README.md index 137d134c..bb15dc2e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,10 +27,6 @@ You can run the native version with `cargo run`: cargo run --package tour ``` -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! - -[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage - ## [Todos](todos) A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. @@ -46,7 +42,6 @@ You can run the native version with `cargo run`: ``` cargo run --package todos ``` -We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! [TodoMVC]: http://todomvc.com/ @@ -105,6 +100,7 @@ A bunch of simpler examples exist: - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. - [`scrollable`](scrollable), a showcase of the various scrollbar width options. +- [`sierpinski_triangle`](sierpinski_triangle), a [sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use `Canvas` and `Slider`. - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. - [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time. - [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. diff --git a/examples/arc/Cargo.toml b/examples/arc/Cargo.toml new file mode 100644 index 00000000..e6e74363 --- /dev/null +++ b/examples/arc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "arc" +version = "0.1.0" +authors = ["ThatsNoMoon <git@thatsnomoon.dev>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } diff --git a/examples/arc/README.md b/examples/arc/README.md new file mode 100644 index 00000000..303253da --- /dev/null +++ b/examples/arc/README.md @@ -0,0 +1,14 @@ +## Arc + +An application that uses the `Canvas` widget to draw a rotating arc. + +This is a simple demo for https://github.com/iced-rs/iced/pull/1358. + +The __[`main`]__ file contains all the code of the example. + +You can run it with `cargo run`: +``` +cargo run --package arc +``` + +[`main`]: src/main.rs diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs new file mode 100644 index 00000000..0c619dc9 --- /dev/null +++ b/examples/arc/src/main.rs @@ -0,0 +1,126 @@ +use std::{f32::consts::PI, time::Instant}; + +use iced::executor; +use iced::widget::canvas::{ + self, Cache, Canvas, Cursor, Geometry, Path, Stroke, +}; +use iced::{ + Application, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, +}; + +pub fn main() -> iced::Result { + Arc::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct Arc { + start: Instant, + cache: Cache, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick, +} + +impl Application for Arc { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Arc { + start: Instant::now(), + cache: Default::default(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Arc - Iced") + } + + fn update(&mut self, _: Message) -> Command<Message> { + self.cache.clear(); + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + iced::time::every(std::time::Duration::from_millis(10)) + .map(|_| Message::Tick) + } + + fn view(&self) -> Element<Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +impl<Message> canvas::Program<Message> for Arc { + type State = (); + + fn draw( + &self, + _state: &Self::State, + theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { + let geometry = self.cache.draw(bounds.size(), |frame| { + let palette = theme.palette(); + + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 5.0; + + let start = Point::new(center.x, center.y - radius); + + let angle = (self.start.elapsed().as_millis() % 10_000) as f32 + / 10_000.0 + * 2.0 + * PI; + + let end = Point::new( + center.x + radius * angle.cos(), + center.y + radius * angle.sin(), + ); + + let circles = Path::new(|b| { + b.circle(start, 10.0); + b.move_to(end); + b.circle(end, 10.0); + }); + + frame.fill(&circles, palette.text); + + let path = Path::new(|b| { + b.move_to(start); + b.arc_to(center, end, 50.0); + b.line_to(end); + }); + + frame.stroke( + &path, + Stroke { + color: palette.text, + width: 10.0, + ..Stroke::default() + }, + ); + }); + + vec![geometry] + } +} diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 35b5182c..7c3916d4 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,7 +1,6 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::{ - button, Alignment, Button, Column, Element, Length, Sandbox, Settings, Text, -}; +use iced::widget::{button, column, text}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings { @@ -14,7 +13,6 @@ pub fn main() -> iced::Result { struct Example { bezier: bezier::State, curves: Vec<bezier::Curve>, - button_state: button::State, } #[derive(Debug, Clone, Copy)] @@ -47,44 +45,34 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - Column::new() - .padding(20) - .spacing(20) - .align_items(Alignment::Center) - .push( - Text::new("Bezier tool example") - .width(Length::Shrink) - .size(50), - ) - .push(self.bezier.view(&self.curves).map(Message::AddCurve)) - .push( - Button::new(&mut self.button_state, Text::new("Clear")) - .padding(8) - .on_press(Message::Clear), - ) - .into() + fn view(&self) -> Element<Message> { + column![ + text("Bezier tool example").width(Length::Shrink).size(50), + self.bezier.view(&self.curves).map(Message::AddCurve), + button("Clear").padding(8).on_press(Message::Clear), + ] + .padding(20) + .spacing(20) + .align_items(Alignment::Center) + .into() } } mod bezier { - use iced::{ - canvas::event::{self, Event}, - canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke}, - mouse, Element, Length, Point, Rectangle, + use iced::mouse; + use iced::widget::canvas::event::{self, Event}; + use iced::widget::canvas::{ + self, Canvas, Cursor, Frame, Geometry, Path, Stroke, }; + use iced::{Element, Length, Point, Rectangle, Theme}; #[derive(Default)] pub struct State { - pending: Option<Pending>, cache: canvas::Cache, } impl State { - pub fn view<'a>( - &'a mut self, - curves: &'a [Curve], - ) -> Element<'a, Curve> { + pub fn view<'a>(&'a self, curves: &'a [Curve]) -> Element<'a, Curve> { Canvas::new(Bezier { state: self, curves, @@ -100,13 +88,16 @@ mod bezier { } struct Bezier<'a> { - state: &'a mut State, + state: &'a State, curves: &'a [Curve], } impl<'a> canvas::Program<Curve> for Bezier<'a> { + type State = Option<Pending>; + fn update( - &mut self, + &self, + state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, @@ -122,16 +113,16 @@ mod bezier { Event::Mouse(mouse_event) => { let message = match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { - match self.state.pending { + match *state { None => { - self.state.pending = Some(Pending::One { + *state = Some(Pending::One { from: cursor_position, }); None } Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { + *state = Some(Pending::Two { from, to: cursor_position, }); @@ -139,7 +130,7 @@ mod bezier { None } Some(Pending::Two { from, to }) => { - self.state.pending = None; + *state = None; Some(Curve { from, @@ -158,18 +149,24 @@ mod bezier { } } - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + fn draw( + &self, + state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { let content = self.state.cache.draw(bounds.size(), |frame: &mut Frame| { Curve::draw_all(self.curves, frame); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default(), + Stroke::default().with_width(2.0), ); }); - if let Some(pending) = &self.state.pending { + if let Some(pending) = state { let pending_curve = pending.draw(bounds, cursor); vec![content, pending_curve] @@ -180,6 +177,7 @@ mod bezier { fn mouse_interaction( &self, + _state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 325ccc1a..8818fb54 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,7 +1,9 @@ +use iced::executor; +use iced::widget::canvas::{Cache, Cursor, Geometry, LineCap, Path, Stroke}; +use iced::widget::{canvas, container}; use iced::{ - canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, - executor, Application, Color, Command, Container, Element, Length, Point, - Rectangle, Settings, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Settings, + Subscription, Theme, Vector, }; pub fn main() -> iced::Result { @@ -22,8 +24,9 @@ enum Message { } impl Application for Clock { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command<Message>) { @@ -65,10 +68,12 @@ impl Application for Clock { }) } - fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new(self).width(Length::Fill).height(Length::Fill); + fn view(&self) -> Element<Message> { + let canvas = canvas(self as &Self) + .width(Length::Fill) + .height(Length::Fill); - Container::new(canvas) + container(canvas) .width(Length::Fill) .height(Length::Fill) .padding(20) @@ -76,8 +81,16 @@ impl Application for Clock { } } -impl canvas::Program<Message> for Clock { - fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { +impl<Message> canvas::Program<Message> for Clock { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { let clock = self.clock.draw(bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 23670b46..8fd37202 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "palette"] } -palette = "0.5.0" +palette = "0.6.0" diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md index 95a23f48..f90020b1 100644 --- a/examples/color_palette/README.md +++ b/examples/color_palette/README.md @@ -11,5 +11,5 @@ A color palette generator, based on a user-defined root color. You can run it with `cargo run`: ``` -cargo run --package color_palette +cargo run --package pure_color_palette ``` diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index ad3004b0..42149965 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,9 +1,10 @@ -use iced::canvas::{self, Cursor, Frame, Geometry, Path}; +use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}; +use iced::widget::{column, row, text, Slider}; use iced::{ - alignment, slider, Alignment, Canvas, Color, Column, Element, Length, - Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector, + alignment, Alignment, Color, Element, Length, Point, Rectangle, Sandbox, + Settings, Size, Vector, }; -use palette::{self, Hsl, Limited, Srgb}; +use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; use std::ops::RangeInclusive; @@ -49,42 +50,43 @@ impl Sandbox for ColorPalette { fn update(&mut self, message: Message) { let srgb = match message { Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), - Message::HslColorChanged(hsl) => palette::Srgb::from(hsl), - Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv), - Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb), - Message::LabColorChanged(lab) => palette::Srgb::from(lab), - Message::LchColorChanged(lch) => palette::Srgb::from(lch), + Message::HslColorChanged(hsl) => palette::Srgb::from_color(hsl), + Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv), + Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb), + Message::LabColorChanged(lab) => palette::Srgb::from_color(lab), + Message::LchColorChanged(lch) => palette::Srgb::from_color(lch), }; - self.theme = Theme::new(srgb.clamp()); + self.theme = Theme::new(srgb); } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let base = self.theme.base; let srgb = palette::Srgb::from(base); - let hsl = palette::Hsl::from(srgb); - let hsv = palette::Hsv::from(srgb); - let hwb = palette::Hwb::from(srgb); - let lab = palette::Lab::from(srgb); - let lch = palette::Lch::from(srgb); - - Column::new() - .padding(10) - .spacing(10) - .push(self.rgb.view(base).map(Message::RgbColorChanged)) - .push(self.hsl.view(hsl).map(Message::HslColorChanged)) - .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) - .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) - .push(self.lab.view(lab).map(Message::LabColorChanged)) - .push(self.lch.view(lch).map(Message::LchColorChanged)) - .push(self.theme.view()) - .into() + let hsl = palette::Hsl::from_color(srgb); + let hsv = palette::Hsv::from_color(srgb); + let hwb = palette::Hwb::from_color(srgb); + let lab = palette::Lab::from_color(srgb); + let lch = palette::Lch::from_color(srgb); + + column![ + self.rgb.view(base).map(Message::RgbColorChanged), + self.hsl.view(hsl).map(Message::HslColorChanged), + self.hsv.view(hsv).map(Message::HsvColorChanged), + self.hwb.view(hwb).map(Message::HwbColorChanged), + self.lab.view(lab).map(Message::LabColorChanged), + self.lch.view(lch).map(Message::LchColorChanged), + self.theme.view(), + ] + .padding(10) + .spacing(10) + .into() } } #[derive(Debug)] -pub struct Theme { +struct Theme { lower: Vec<Color>, base: Color, higher: Vec<Color>, @@ -98,7 +100,7 @@ impl Theme { let base = base.into(); // Convert to HSL color for manipulation - let hsl = Hsl::from(Srgb::from(base)); + let hsl = Hsl::from_color(Srgb::from(base)); let lower = [ hsl.shift_hue(-135.0).lighten(0.075), @@ -117,12 +119,12 @@ impl Theme { Theme { lower: lower .iter() - .map(|&color| Srgb::from(color).clamp().into()) + .map(|&color| Srgb::from_color(color).into()) .collect(), base, higher: higher .iter() - .map(|&color| Srgb::from(color).clamp().into()) + .map(|&color| Srgb::from_color(color).into()) .collect(), canvas_cache: canvas::Cache::default(), } @@ -139,7 +141,7 @@ impl Theme { .chain(self.higher.iter()) } - pub fn view(&mut self) -> Element<Message> { + pub fn view(&self) -> Element<Message> { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -207,14 +209,14 @@ impl Theme { text.vertical_alignment = alignment::Vertical::Bottom; - let hsl = Hsl::from(Srgb::from(self.base)); + let hsl = Hsl::from_color(Srgb::from(self.base)); for i in 0..self.len() { let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); let graded = Hsl { lightness: 1.0 - pct, ..hsl }; - let color: Color = Srgb::from(graded.clamp()).into(); + let color: Color = Srgb::from_color(graded).into(); let anchor = Point { x: (i as f32) * box_size.width, @@ -235,8 +237,16 @@ impl Theme { } } -impl canvas::Program<Message> for Theme { - fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { +impl<Message> canvas::Program<Message> for Theme { + type State = (); + + fn draw( + &self, + _state: &Self::State, + _theme: &iced::Theme, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<Geometry> { let theme = self.canvas_cache.draw(bounds.size(), |frame| { self.draw(frame); }); @@ -262,7 +272,6 @@ fn color_hex_string(color: &Color) -> String { #[derive(Default)] struct ColorPicker<C: ColorSpace> { - sliders: [slider::State; 3], color_space: PhantomData<C>, } @@ -277,37 +286,30 @@ trait ColorSpace: Sized { fn to_string(&self) -> String; } -impl<C: 'static + ColorSpace + Copy> ColorPicker<C> { - fn view(&mut self, color: C) -> Element<C> { +impl<C: ColorSpace + Copy> ColorPicker<C> { + fn view(&self, color: C) -> Element<C> { let [c1, c2, c3] = color.components(); - let [s1, s2, s3] = &mut self.sliders; let [cr1, cr2, cr3] = C::COMPONENT_RANGES; - fn slider<C: Clone>( - state: &mut slider::State, + fn slider<'a, C: Clone>( range: RangeInclusive<f64>, component: f32, - update: impl Fn(f32) -> C + 'static, - ) -> Slider<f64, C> { - Slider::new(state, range, f64::from(component), move |v| { - update(v as f32) - }) - .step(0.01) + update: impl Fn(f32) -> C + 'a, + ) -> Slider<'a, f64, C, iced::Renderer> { + Slider::new(range, f64::from(component), move |v| update(v as f32)) + .step(0.01) } - Row::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new(C::LABEL).width(Length::Units(50))) - .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3))) - .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3))) - .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v))) - .push( - Text::new(color.to_string()) - .width(Length::Units(185)) - .size(14), - ) - .into() + row![ + text(C::LABEL).width(Length::Units(50)), + slider(cr1, c1, move |v| C::new(v, c2, c3)), + slider(cr2, c2, move |v| C::new(c1, v, c3)), + slider(cr3, c3, move |v| C::new(c1, c2, v)), + text(color.to_string()).width(Length::Units(185)).size(14), + ] + .spacing(10) + .align_items(Alignment::Center) + .into() } } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 39335cf1..06b1e53a 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -1,5 +1,7 @@ -use iced::{Container, Element, Length, Sandbox, Settings}; -use numeric_input::NumericInput; +use iced::widget::container; +use iced::{Element, Length, Sandbox, Settings}; + +use numeric_input::numeric_input; pub fn main() -> iced::Result { Component::run(Settings::default()) @@ -7,7 +9,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Component { - numeric_input: numeric_input::State, value: Option<u32>, } @@ -35,39 +36,31 @@ impl Sandbox for Component { } } - fn view(&mut self) -> Element<Message> { - Container::new(NumericInput::new( - &mut self.numeric_input, - self.value, - Message::NumericInputChanged, - )) - .padding(20) - .height(Length::Fill) - .center_y() - .into() + fn view(&self) -> Element<Message> { + container(numeric_input(self.value, Message::NumericInputChanged)) + .padding(20) + .height(Length::Fill) + .center_y() + .into() } } mod numeric_input { - use iced_lazy::component::{self, Component}; - use iced_native::alignment::{self, Alignment}; - use iced_native::text; - use iced_native::widget::button::{self, Button}; - use iced_native::widget::text_input::{self, TextInput}; - use iced_native::widget::{Row, Text}; - use iced_native::{Element, Length}; - - pub struct NumericInput<'a, Message> { - state: &'a mut State, + use iced::alignment::{self, Alignment}; + use iced::widget::{self, button, row, text, text_input}; + use iced::{Element, Length}; + use iced_lazy::{self, Component}; + + pub struct NumericInput<Message> { value: Option<u32>, on_change: Box<dyn Fn(Option<u32>) -> Message>, } - #[derive(Default)] - pub struct State { - input: text_input::State, - decrement_button: button::State, - increment_button: button::State, + pub fn numeric_input<Message>( + value: Option<u32>, + on_change: impl Fn(Option<u32>) -> Message + 'static, + ) -> NumericInput<Message> { + NumericInput::new(value, on_change) } #[derive(Debug, Clone)] @@ -77,28 +70,33 @@ mod numeric_input { DecrementPressed, } - impl<'a, Message> NumericInput<'a, Message> { + impl<Message> NumericInput<Message> { pub fn new( - state: &'a mut State, value: Option<u32>, on_change: impl Fn(Option<u32>) -> Message + 'static, ) -> Self { Self { - state, value, on_change: Box::new(on_change), } } } - impl<'a, Message, Renderer> Component<Message, Renderer> - for NumericInput<'a, Message> + impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message> where - Renderer: 'a + text::Renderer, + Renderer: iced_native::text::Renderer + 'static, + Renderer::Theme: widget::button::StyleSheet + + widget::text_input::StyleSheet + + widget::text::StyleSheet, { + type State = (); type Event = Event; - fn update(&mut self, event: Event) -> Option<Message> { + fn update( + &mut self, + _state: &mut Self::State, + event: Event, + ) -> Option<Message> { match event { Event::IncrementPressed => Some((self.on_change)(Some( self.value.unwrap_or_default().saturating_add(1), @@ -120,11 +118,10 @@ mod numeric_input { } } - fn view(&mut self) -> Element<Event, Renderer> { - let button = |state, label, on_press| { - Button::new( - state, - Text::new(label) + fn view(&self, _state: &Self::State) -> Element<Event, Renderer> { + let button = |label, on_press| { + button( + text(label) .width(Length::Fill) .height(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center) @@ -134,47 +131,37 @@ mod numeric_input { .on_press(on_press) }; - Row::with_children(vec![ - button( - &mut self.state.decrement_button, - "-", - Event::DecrementPressed, - ) - .into(), - TextInput::new( - &mut self.state.input, + row![ + button("-", Event::DecrementPressed), + text_input( "Type a number", self.value .as_ref() .map(u32::to_string) - .as_ref() - .map(String::as_str) + .as_deref() .unwrap_or(""), Event::InputChanged, ) - .padding(10) - .into(), - button( - &mut self.state.increment_button, - "+", - Event::IncrementPressed, - ) - .into(), - ]) + .padding(10), + button("+", Event::IncrementPressed), + ] .align_items(Alignment::Fill) .spacing(10) .into() } } - impl<'a, Message, Renderer> From<NumericInput<'a, Message>> + impl<'a, Message, Renderer> From<NumericInput<Message>> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: text::Renderer + 'a, + Renderer: 'static + iced_native::text::Renderer, + Renderer::Theme: widget::button::StyleSheet + + widget::text_input::StyleSheet + + widget::text::StyleSheet, { - fn from(numeric_input: NumericInput<'a, Message>) -> Self { - component::view(numeric_input) + fn from(numeric_input: NumericInput<Message>) -> Self { + iced_lazy::component(numeric_input) } } } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 931cf5e1..13dcbf86 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,16 +1,12 @@ -use iced::{ - button, Alignment, Button, Column, Element, Sandbox, Settings, Text, -}; +use iced::widget::{button, column, text}; +use iced::{Alignment, Element, Sandbox, Settings}; pub fn main() -> iced::Result { Counter::run(Settings::default()) } -#[derive(Default)] struct Counter { value: i32, - increment_button: button::State, - decrement_button: button::State, } #[derive(Debug, Clone, Copy)] @@ -23,7 +19,7 @@ impl Sandbox for Counter { type Message = Message; fn new() -> Self { - Self::default() + Self { value: 0 } } fn title(&self) -> String { @@ -41,19 +37,14 @@ impl Sandbox for Counter { } } - fn view(&mut self) -> Element<Message> { - Column::new() - .padding(20) - .align_items(Alignment::Center) - .push( - Button::new(&mut self.increment_button, Text::new("Increment")) - .on_press(Message::IncrementPressed), - ) - .push(Text::new(self.value.to_string()).size(50)) - .push( - Button::new(&mut self.decrement_button, Text::new("Decrement")) - .on_press(Message::DecrementPressed), - ) - .into() + fn view(&self) -> Element<Message> { + column![ + button("Increment").on_press(Message::IncrementPressed), + text(self.value).size(50), + button("Decrement").on_press(Message::DecrementPressed) + ] + .padding(20) + .align_items(Alignment::Center) + .into() } } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 28edf256..c37a1a12 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -11,7 +11,8 @@ mod circle { // implemented by `iced_wgpu` and other renderers. use iced_native::layout::{self, Layout}; use iced_native::renderer; - use iced_native::{Color, Element, Length, Point, Rectangle, Size, Widget}; + use iced_native::widget::{self, Widget}; + use iced_native::{Color, Element, Length, Point, Rectangle, Size}; pub struct Circle { radius: f32, @@ -23,6 +24,10 @@ mod circle { } } + pub fn circle(radius: f32) -> Circle { + Circle::new(radius) + } + impl<Message, Renderer> Widget<Message, Renderer> for Circle where Renderer: renderer::Renderer, @@ -45,7 +50,9 @@ mod circle { fn draw( &self, + _state: &widget::Tree, renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -63,21 +70,19 @@ mod circle { } } - impl<'a, Message, Renderer> Into<Element<'a, Message, Renderer>> for Circle + impl<'a, Message, Renderer> From<Circle> for Element<'a, Message, Renderer> where Renderer: renderer::Renderer, { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) + fn from(circle: Circle) -> Self { + Self::new(circle) } } } -use circle::Circle; -use iced::{ - slider, Alignment, Column, Container, Element, Length, Sandbox, Settings, - Slider, Text, -}; +use circle::circle; +use iced::widget::{column, container, slider, text}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings::default()) @@ -85,7 +90,6 @@ pub fn main() -> iced::Result { struct Example { radius: f32, - slider: slider::State, } #[derive(Debug, Clone, Copy)] @@ -97,10 +101,7 @@ impl Sandbox for Example { type Message = Message; fn new() -> Self { - Example { - radius: 50.0, - slider: slider::State::new(), - } + Example { radius: 50.0 } } fn title(&self) -> String { @@ -115,25 +116,18 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Alignment::Center) - .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {:.2}", self.radius))) - .push( - Slider::new( - &mut self.slider, - 1.0..=100.0, - self.radius, - Message::RadiusChanged, - ) - .step(0.01), - ); - - Container::new(content) + fn view(&self) -> Element<Message> { + let content = column![ + circle(self.radius), + text(format!("Radius: {:.2}", self.radius)), + slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01), + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Alignment::Center); + + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index 7db1206b..39dd843f 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -70,9 +70,7 @@ async fn download<I: Copy>( // We do not let the stream die, as it would start a // new download repeatedly if the user is not careful // in case of errors. - let _: () = iced::futures::future::pending().await; - - unreachable!() + iced::futures::future::pending().await } } } diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 21804a0a..3ef9ef7a 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,6 +1,8 @@ +use iced::executor; +use iced::widget::{button, column, container, progress_bar, text, Column}; use iced::{ - button, executor, Alignment, Application, Button, Column, Command, - Container, Element, Length, ProgressBar, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; mod download; @@ -13,7 +15,6 @@ pub fn main() -> iced::Result { struct Example { downloads: Vec<Download>, last_id: usize, - add: button::State, } #[derive(Debug, Clone)] @@ -24,8 +25,9 @@ pub enum Message { } impl Application for Example { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Example, Command<Message>) { @@ -33,7 +35,6 @@ impl Application for Example { Example { downloads: vec![Download::new(0)], last_id: 0, - add: button::State::new(), }, Command::none(), ) @@ -46,7 +47,7 @@ impl Application for Example { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Add => { - self.last_id = self.last_id + 1; + self.last_id += 1; self.downloads.push(Download::new(self.last_id)); } @@ -71,21 +72,19 @@ impl Application for Example { Subscription::batch(self.downloads.iter().map(Download::subscription)) } - fn view(&mut self) -> Element<Message> { - let downloads = self - .downloads - .iter_mut() - .fold(Column::new().spacing(20), |column, download| { - column.push(download.view()) - }) - .push( - Button::new(&mut self.add, Text::new("Add another download")) - .on_press(Message::Add) - .padding(10), - ) - .align_items(Alignment::End); - - Container::new(downloads) + 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); + + container(downloads) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -103,19 +102,17 @@ struct Download { #[derive(Debug)] enum State { - Idle { button: button::State }, + Idle, Downloading { progress: f32 }, - Finished { button: button::State }, - Errored { button: button::State }, + Finished, + Errored, } impl Download { pub fn new(id: usize) -> Self { Download { id, - state: State::Idle { - button: button::State::new(), - }, + state: State::Idle, } } @@ -131,8 +128,8 @@ impl Download { } pub fn progress(&mut self, new_progress: download::Progress) { - match &mut self.state { - State::Downloading { progress } => match new_progress { + if let State::Downloading { progress } = &mut self.state { + match new_progress { download::Progress::Started => { *progress = 0.0; } @@ -140,17 +137,12 @@ impl Download { *progress = percentage; } download::Progress::Finished => { - self.state = State::Finished { - button: button::State::new(), - } + self.state = State::Finished; } download::Progress::Errored => { - self.state = State::Errored { - button: button::State::new(), - }; + self.state = State::Errored; } - }, - _ => {} + } } } @@ -164,7 +156,7 @@ impl Download { } } - pub fn view(&mut self) -> Element<Message> { + pub fn view(&self) -> Element<Message> { let current_progress = match &self.state { State::Idle { .. } => 0.0, State::Downloading { progress } => *progress, @@ -172,36 +164,28 @@ impl Download { State::Errored { .. } => 0.0, }; - let progress_bar = ProgressBar::new(0.0..=100.0, current_progress); + let progress_bar = progress_bar(0.0..=100.0, current_progress); - let control: Element<_> = match &mut self.state { - State::Idle { button } => { - Button::new(button, Text::new("Start the download!")) - .on_press(Message::Download(self.id)) + let control: Element<_> = match &self.state { + State::Idle => button("Start the download!") + .on_press(Message::Download(self.id)) + .into(), + State::Finished => { + column!["Download finished!", button("Start again")] + .spacing(10) + .align_items(Alignment::Center) .into() } - State::Finished { button } => Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Download finished!")) - .push( - Button::new(button, Text::new("Start again")) - .on_press(Message::Download(self.id)), - ) - .into(), State::Downloading { .. } => { - Text::new(format!("Downloading... {:.2}%", current_progress)) - .into() + text(format!("Downloading... {:.2}%", current_progress)).into() } - State::Errored { button } => Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Something went wrong :(")) - .push( - Button::new(button, Text::new("Try again")) - .on_press(Message::Download(self.id)), - ) - .into(), + State::Errored => column![ + "Something went wrong :(", + button("Try again").on_press(Message::Download(self.id)), + ] + .spacing(10) + .align_items(Alignment::Center) + .into(), }; Column::new() diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 7f024c56..234e1423 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,6 +1,9 @@ +use iced::alignment; +use iced::executor; +use iced::widget::{button, checkbox, container, text, Column}; use iced::{ - alignment, button, executor, Alignment, Application, Button, Checkbox, - Column, Command, Container, Element, Length, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use iced_native::{window, Event}; @@ -15,7 +18,6 @@ pub fn main() -> iced::Result { struct Events { last: Vec<iced_native::Event>, enabled: bool, - exit: button::State, should_exit: bool, } @@ -27,8 +29,9 @@ enum Message { } impl Application for Events { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Events, Command<Message>) { @@ -72,23 +75,23 @@ impl Application for Events { self.should_exit } - fn view(&mut self) -> Element<Message> { - let events = self.last.iter().fold( - Column::new().spacing(10), - |column, event| { - column.push(Text::new(format!("{:?}", event)).size(40)) - }, + fn view(&self) -> Element<Message> { + let events = Column::with_children( + self.last + .iter() + .map(|event| text(format!("{:?}", event)).size(40)) + .map(Element::from) + .collect(), ); - let toggle = Checkbox::new( - self.enabled, + let toggle = checkbox( "Listen to runtime events", + self.enabled, Message::Toggled, ); - let exit = Button::new( - &mut self.exit, - Text::new("Exit") + let exit = button( + text("Exit") .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) @@ -103,7 +106,7 @@ impl Application for Events { .push(toggle) .push(exit); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index c45a8205..5d518d2f 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,7 +1,5 @@ -use iced::{ - button, Alignment, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, -}; +use iced::widget::{button, column, container}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Exit::run(Settings::default()) @@ -11,8 +9,6 @@ pub fn main() -> iced::Result { struct Exit { show_confirm: bool, exit: bool, - confirm_button: button::State, - exit_button: button::State, } #[derive(Debug, Clone, Copy)] @@ -47,33 +43,24 @@ impl Sandbox for Exit { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let content = if self.show_confirm { - Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Are you sure you want to exit?")) - .push( - Button::new( - &mut self.confirm_button, - Text::new("Yes, exit now"), - ) + column![ + "Are you sure you want to exit?", + button("Yes, exit now") .padding([10, 20]) .on_press(Message::Confirm), - ) + ] } else { - Column::new() - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new("Click the button to exit")) - .push( - Button::new(&mut self.exit_button, Text::new("Exit")) - .padding([10, 20]) - .on_press(Message::Exit), - ) - }; + column![ + "Click the button to exit", + button("Exit").padding([10, 20]).on_press(Message::Exit), + ] + } + .spacing(10) + .align_items(Alignment::Center); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ab8b80e4..a2030275 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,20 +1,20 @@ //! This example showcases an interactive version of the Game of Life, invented //! by John Conway. It leverages a `Canvas` together with other widgets. mod preset; -mod style; use grid::Grid; -use iced::button::{self, Button}; +use preset::Preset; + use iced::executor; -use iced::pick_list::{self, PickList}; -use iced::slider::{self, Slider}; +use iced::theme::{self, Theme}; use iced::time; +use iced::widget::{ + button, checkbox, column, container, pick_list, row, slider, text, +}; use iced::window; use iced::{ - Alignment, Application, Checkbox, Column, Command, Container, Element, - Length, Row, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, }; -use preset::Preset; use std::time::{Duration, Instant}; pub fn main() -> iced::Result { @@ -33,7 +33,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct GameOfLife { grid: Grid, - controls: Controls, is_playing: bool, queued_ticks: usize, speed: usize, @@ -55,6 +54,7 @@ enum Message { impl Application for GameOfLife { type Message = Message; + type Theme = Theme; type Executor = executor::Default; type Flags = (); @@ -131,39 +131,87 @@ impl Application for GameOfLife { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let version = self.version; let selected_speed = self.next_speed.unwrap_or(self.speed); - let controls = self.controls.view( + let controls = view_controls( self.is_playing, self.grid.are_lines_visible(), selected_speed, self.grid.preset(), ); - let content = Column::new() - .push( - self.grid - .view() - .map(move |message| Message::Grid(message, version)), - ) - .push(controls); + let content = column![ + self.grid + .view() + .map(move |message| Message::Grid(message, version)), + controls + ]; - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) - .style(style::Container) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +fn view_controls<'a>( + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + preset: Preset, +) -> Element<'a, Message> { + let playback_controls = row![ + button(if is_playing { "Pause" } else { "Play" }) + .on_press(Message::TogglePlayback), + button("Next") + .on_press(Message::Next) + .style(theme::Button::Secondary), + ] + .spacing(10); + + let speed_controls = row![ + 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); + + row![ + playback_controls, + speed_controls, + checkbox("Grid", is_grid_enabled, Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + pick_list(preset::ALL, Some(preset), Message::PresetPicked) + .padding(8) + .text_size(16), + button("Clear") + .on_press(Message::Clear) + .style(theme::Button::Destructive), + ] + .padding(10) + .spacing(20) + .align_items(Alignment::Center) + .into() } mod grid { use crate::Preset; + use iced::widget::canvas; + use iced::widget::canvas::event::{self, Event}; + use iced::widget::canvas::{ + Cache, Canvas, Cursor, Frame, Geometry, Path, Text, + }; use iced::{ - alignment, - canvas::event::{self, Event}, - canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text}, - mouse, Color, Element, Length, Point, Rectangle, Size, Vector, + alignment, mouse, Color, Element, Length, Point, Rectangle, Size, + Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -173,7 +221,6 @@ mod grid { pub struct Grid { state: State, preset: Preset, - interaction: Interaction, life_cache: Cache, grid_cache: Cache, translation: Vector, @@ -187,6 +234,8 @@ mod grid { pub enum Message { Populate(Cell), Unpopulate(Cell), + Translated(Vector), + Scaled(f32, Option<Vector>), Ticked { result: Result<Life, TickError>, tick_duration: Duration, @@ -218,7 +267,6 @@ mod grid { .collect(), ), preset, - interaction: Interaction::None, life_cache: Cache::default(), grid_cache: Cache::default(), translation: Vector::default(), @@ -263,6 +311,22 @@ mod grid { self.preset = Preset::Custom; } + Message::Translated(translation) => { + self.translation = translation; + + self.life_cache.clear(); + self.grid_cache.clear(); + } + Message::Scaled(scaling, translation) => { + self.scaling = scaling; + + if let Some(translation) = translation { + self.translation = translation; + } + + self.life_cache.clear(); + self.grid_cache.clear(); + } Message::Ticked { result: Ok(life), tick_duration, @@ -280,7 +344,7 @@ mod grid { } } - pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + pub fn view(&self) -> Element<Message> { Canvas::new(self) .width(Length::Fill) .height(Length::Fill) @@ -328,15 +392,18 @@ mod grid { } } - impl<'a> canvas::Program<Message> for Grid { + impl canvas::Program<Message> for Grid { + type State = Interaction; + fn update( - &mut self, + &self, + interaction: &mut Interaction, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option<Message>) { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - self.interaction = Interaction::None; + *interaction = Interaction::None; } let cursor_position = @@ -360,7 +427,7 @@ mod grid { mouse::Event::ButtonPressed(button) => { let message = match button { mouse::Button::Left => { - self.interaction = if is_populated { + *interaction = if is_populated { Interaction::Erasing } else { Interaction::Drawing @@ -369,7 +436,7 @@ mod grid { populate.or(unpopulate) } mouse::Button::Right => { - self.interaction = Interaction::Panning { + *interaction = Interaction::Panning { translation: self.translation, start: cursor_position, }; @@ -382,23 +449,20 @@ mod grid { (event::Status::Captured, message) } mouse::Event::CursorMoved { .. } => { - let message = match self.interaction { + let message = match *interaction { Interaction::Drawing => populate, Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { - self.translation = translation - + (cursor_position - start) - * (1.0 / self.scaling); - - self.life_cache.clear(); - self.grid_cache.clear(); - - None + Some(Message::Translated( + translation + + (cursor_position - start) + * (1.0 / self.scaling), + )) } _ => None, }; - let event_status = match self.interaction { + let event_status = match interaction { Interaction::None => event::Status::Ignored, _ => event::Status::Captured, }; @@ -413,30 +477,38 @@ mod grid { { let old_scaling = self.scaling; - self.scaling = (self.scaling - * (1.0 + y / 30.0)) + let scaling = (self.scaling * (1.0 + y / 30.0)) .max(Self::MIN_SCALING) .min(Self::MAX_SCALING); - if let Some(cursor_to_center) = - cursor.position_from(bounds.center()) - { - let factor = self.scaling - old_scaling; - - self.translation = self.translation - - Vector::new( - cursor_to_center.x * factor - / (old_scaling * old_scaling), - cursor_to_center.y * factor - / (old_scaling * old_scaling), - ); - } - - self.life_cache.clear(); - self.grid_cache.clear(); + let translation = + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + let factor = scaling - old_scaling; + + Some( + self.translation + - Vector::new( + cursor_to_center.x * factor + / (old_scaling + * old_scaling), + cursor_to_center.y * factor + / (old_scaling + * old_scaling), + ), + ) + } else { + None + }; + + ( + event::Status::Captured, + Some(Message::Scaled(scaling, translation)), + ) + } else { + (event::Status::Captured, None) } - - (event::Status::Captured, None) } }, _ => (event::Status::Ignored, None), @@ -445,7 +517,13 @@ mod grid { } } - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + fn draw( + &self, + _interaction: &Interaction, + _theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); let life = self.life_cache.draw(bounds.size(), |frame| { @@ -571,10 +649,11 @@ mod grid { fn mouse_interaction( &self, + interaction: &Interaction, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { - match self.interaction { + match interaction { Interaction::Drawing => mouse::Interaction::Crosshair, Interaction::Erasing => mouse::Interaction::Crosshair, Interaction::Panning { .. } => mouse::Interaction::Grabbing, @@ -803,90 +882,16 @@ mod grid { } } - enum Interaction { + pub enum Interaction { None, Drawing, Erasing, Panning { translation: Vector, start: Point }, } -} - -#[derive(Default)] -struct Controls { - toggle_button: button::State, - next_button: button::State, - clear_button: button::State, - speed_slider: slider::State, - preset_list: pick_list::State<Preset>, -} -impl Controls { - fn view<'a>( - &'a mut self, - is_playing: bool, - is_grid_enabled: bool, - speed: usize, - preset: Preset, - ) -> Element<'a, Message> { - let playback_controls = Row::new() - .spacing(10) - .push( - Button::new( - &mut self.toggle_button, - Text::new(if is_playing { "Pause" } else { "Play" }), - ) - .on_press(Message::TogglePlayback) - .style(style::Button), - ) - .push( - Button::new(&mut self.next_button, Text::new("Next")) - .on_press(Message::Next) - .style(style::Button), - ); - - let speed_controls = Row::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push( - Slider::new( - &mut self.speed_slider, - 1.0..=1000.0, - speed as f32, - Message::SpeedChanged, - ) - .style(style::Slider), - ) - .push(Text::new(format!("x{}", speed)).size(16)); - - Row::new() - .padding(10) - .spacing(20) - .align_items(Alignment::Center) - .push(playback_controls) - .push(speed_controls) - .push( - Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) - .size(16) - .spacing(5) - .text_size(16), - ) - .push( - PickList::new( - &mut self.preset_list, - preset::ALL, - Some(preset), - Message::PresetPicked, - ) - .padding(8) - .text_size(16) - .style(style::PickList), - ) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Clear), - ) - .into() + impl Default for Interaction { + fn default() -> Self { + Self::None + } } } diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs index 05157b6a..964b9120 100644 --- a/examples/game_of_life/src/preset.rs +++ b/examples/game_of_life/src/preset.rs @@ -1,7 +1,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Preset { Custom, - XKCD, + Xkcd, Glider, SmallExploder, Exploder, @@ -14,7 +14,7 @@ pub enum Preset { pub static ALL: &[Preset] = &[ Preset::Custom, - Preset::XKCD, + Preset::Xkcd, Preset::Glider, Preset::SmallExploder, Preset::Exploder, @@ -30,7 +30,7 @@ impl Preset { #[rustfmt::skip] let cells = match self { Preset::Custom => vec![], - Preset::XKCD => vec![ + Preset::Xkcd => vec![ " xxx ", " x x ", " x x ", @@ -116,7 +116,7 @@ impl Preset { impl Default for Preset { fn default() -> Preset { - Preset::XKCD + Preset::Xkcd } } @@ -127,7 +127,7 @@ impl std::fmt::Display for Preset { "{}", match self { Preset::Custom => "Custom", - Preset::XKCD => "xkcd #2293", + Preset::Xkcd => "xkcd #2293", Preset::Glider => "Glider", Preset::SmallExploder => "Small Exploder", Preset::Exploder => "Exploder", diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs deleted file mode 100644 index be9a0e96..00000000 --- a/examples/game_of_life/src/style.rs +++ /dev/null @@ -1,189 +0,0 @@ -use iced::{button, container, pick_list, slider, Background, Color}; - -const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, -); - -const DESTRUCTIVE: Color = Color::from_rgb( - 0xC0 as f32 / 255.0, - 0x47 as f32 / 255.0, - 0x47 as f32 / 255.0, -); - -const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, -); - -const BACKGROUND: Color = Color::from_rgb( - 0x2F as f32 / 255.0, - 0x31 as f32 / 255.0, - 0x36 as f32 / 255.0, -); - -pub struct Container; - -impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(Color::from_rgb8( - 0x36, 0x39, 0x3F, - ))), - text_color: Some(Color::WHITE), - ..container::Style::default() - } - } -} - -pub struct Button; - -impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(HOVERED)), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } -} - -pub struct Clear; - -impl button::StyleSheet for Clear { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(DESTRUCTIVE)), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(Color { - a: 0.5, - ..DESTRUCTIVE - })), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } -} - -pub struct Slider; - -impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9.0 }, - color: ACTIVE, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } -} - -pub struct PickList; - -impl pick_list::StyleSheet for PickList { - fn menu(&self) -> pick_list::Menu { - pick_list::Menu { - text_color: Color::WHITE, - background: BACKGROUND.into(), - border_width: 1.0, - border_color: Color { - a: 0.7, - ..Color::BLACK - }, - selected_background: Color { - a: 0.5, - ..Color::BLACK - } - .into(), - selected_text_color: Color::WHITE, - } - } - - fn active(&self) -> pick_list::Style { - pick_list::Style { - text_color: Color::WHITE, - background: BACKGROUND.into(), - border_width: 1.0, - border_color: Color { - a: 0.6, - ..Color::BLACK - }, - border_radius: 2.0, - icon_size: 0.5, - ..pick_list::Style::default() - } - } - - fn hovered(&self) -> pick_list::Style { - let active = self.active(); - - pick_list::Style { - border_color: Color { - a: 0.9, - ..Color::BLACK - }, - ..active - } - } -} diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 58dfa3ad..d8b99ab3 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -13,10 +13,12 @@ mod rainbow { use iced_graphics::renderer::{self, Renderer}; use iced_graphics::{Backend, Primitive}; + use iced_native::widget::{self, Widget}; use iced_native::{ - layout, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, + layout, Element, Layout, Length, Point, Rectangle, Size, Vector, }; + #[derive(Default)] pub struct Rainbow; impl Rainbow { @@ -25,7 +27,11 @@ mod rainbow { } } - impl<Message, B> Widget<Message, Renderer<B>> for Rainbow + pub fn rainbow() -> Rainbow { + Rainbow + } + + impl<Message, B, T> Widget<Message, Renderer<B, T>> for Rainbow where B: Backend, { @@ -39,7 +45,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -49,7 +55,9 @@ mod rainbow { fn draw( &self, - renderer: &mut Renderer<B>, + _tree: &widget::Tree, + renderer: &mut Renderer<B, T>, + _theme: &T, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -147,37 +155,31 @@ mod rainbow { } } - impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow + impl<'a, Message, B, T> From<Rainbow> for Element<'a, Message, Renderer<B, T>> where B: Backend, { - fn into(self) -> Element<'a, Message, Renderer<B>> { - Element::new(self) + fn from(rainbow: Rainbow) -> Self { + Self::new(rainbow) } } } -use iced::{ - scrollable, Alignment, Column, Container, Element, Length, Sandbox, - Scrollable, Settings, Text, -}; -use rainbow::Rainbow; +use iced::widget::{column, container, scrollable}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; +use rainbow::rainbow; pub fn main() -> iced::Result { Example::run(Settings::default()) } -struct Example { - scroll: scrollable::State, -} +struct Example; impl Sandbox for Example { type Message = (); fn new() -> Self { - Example { - scroll: scrollable::State::new(), - } + Example } fn title(&self) -> String { @@ -186,32 +188,27 @@ impl Sandbox for Example { fn update(&mut self, _: ()) {} - fn view(&mut self) -> Element<()> { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Alignment::Start) - .push(Rainbow::new()) - .push(Text::new( - "In this example we draw a custom widget Rainbow, using \ + fn view(&self) -> Element<()> { + let content = column![ + rainbow(), + "In this example we draw a custom widget Rainbow, using \ the Mesh2D primitive. This primitive supplies a list of \ triangles, expressed as vertices and indices.", - )) - .push(Text::new( - "Move your cursor over it, and see the center vertex \ + "Move your cursor over it, and see the center vertex \ follow you!", - )) - .push(Text::new( - "Every Vertex2D defines its own color. You could use the \ + "Every Vertex2D defines its own color. You could use the \ Mesh2D primitive to render virtually any two-dimensional \ geometry for your widget.", - )); + ] + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Alignment::Start); - let scrollable = Scrollable::new(&mut self.scroll) - .push(Container::new(content).width(Length::Fill).center_x()); + let scrollable = + scrollable(container(content).width(Length::Fill).center_x()); - Container::new(scrollable) + container(scrollable) .width(Length::Fill) .height(Length::Fill) .center_y() diff --git a/examples/integration_opengl/src/controls.rs b/examples/integration_opengl/src/controls.rs index f387b4e5..076d37d3 100644 --- a/examples/integration_opengl/src/controls.rs +++ b/examples/integration_opengl/src/controls.rs @@ -1,11 +1,10 @@ use iced_glow::Renderer; -use iced_glutin::widget::slider::{self, Slider}; +use iced_glutin::widget::Slider; use iced_glutin::widget::{Column, Row, Text}; use iced_glutin::{Alignment, Color, Command, Element, Length, Program}; pub struct Controls { background_color: Color, - sliders: [slider::State; 3], } #[derive(Debug, Clone)] @@ -17,7 +16,6 @@ impl Controls { pub fn new() -> Controls { Controls { background_color: Color::BLACK, - sliders: Default::default(), } } @@ -40,15 +38,14 @@ impl Program for Controls { Command::none() } - fn view(&mut self) -> Element<Message, Renderer> { - let [r, g, b] = &mut self.sliders; + fn view(&self) -> Element<Message, Renderer> { let background_color = self.background_color; let sliders = Row::new() .width(Length::Units(500)) .spacing(20) .push( - Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + Slider::new(0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color @@ -57,7 +54,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + Slider::new(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color @@ -66,7 +63,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(b, 0.0..=1.0, background_color.b, move |b| { + Slider::new(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color @@ -89,13 +86,13 @@ impl Program for Controls { .spacing(10) .push( Text::new("Background color") - .color(Color::WHITE), + .style(Color::WHITE), ) .push(sliders) .push( Text::new(format!("{:?}", background_color)) .size(14) - .color(Color::WHITE), + .style(Color::WHITE), ), ), ) diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index 1007b90f..f161c8a0 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -12,7 +12,8 @@ use iced_glow::glow; use iced_glow::{Backend, Renderer, Settings, Viewport}; use iced_glutin::conversion; use iced_glutin::glutin; -use iced_glutin::{program, Clipboard, Debug, Size}; +use iced_glutin::renderer; +use iced_glutin::{program, Clipboard, Color, Debug, Size}; pub fn main() { env_logger::init(); @@ -57,7 +58,7 @@ pub fn main() { let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(&windowed_context.window()); + let mut clipboard = Clipboard::connect(windowed_context.window()); let mut renderer = Renderer::new(Backend::new(&gl, Settings::default())); @@ -72,13 +73,12 @@ pub fn main() { ); let mut resized = false; - let scene = Scene::new(&gl, &shader_version); + let scene = Scene::new(&gl, shader_version); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { - Event::LoopDestroyed => return, Event::WindowEvent { event, .. } => { match event { WindowEvent::CursorMoved { position, .. } => { @@ -125,6 +125,10 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, + &iced_glow::Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, + }, &mut clipboard, &mut debug, ); diff --git a/examples/integration_wgpu/README.md b/examples/integration_wgpu/README.md index faefa153..ece9ba1e 100644 --- a/examples/integration_wgpu/README.md +++ b/examples/integration_wgpu/README.md @@ -12,7 +12,7 @@ The __[`main`]__ file contains all the code of the example. You can run it with `cargo run`: ``` -cargo run --package integration +cargo run --package integration_wgpu ``` ### How to run this example with WebGL backend diff --git a/examples/integration_wgpu/src/controls.rs b/examples/integration_wgpu/src/controls.rs index 9bca40eb..6c41738c 100644 --- a/examples/integration_wgpu/src/controls.rs +++ b/examples/integration_wgpu/src/controls.rs @@ -1,14 +1,10 @@ use iced_wgpu::Renderer; -use iced_winit::widget::slider::{self, Slider}; -use iced_winit::widget::text_input::{self, TextInput}; -use iced_winit::widget::{Column, Row, Text}; +use iced_winit::widget::{slider, text_input, Column, Row, Text}; use iced_winit::{Alignment, Color, Command, Element, Length, Program}; pub struct Controls { background_color: Color, text: String, - sliders: [slider::State; 3], - text_input: text_input::State, } #[derive(Debug, Clone)] @@ -22,8 +18,6 @@ impl Controls { Controls { background_color: Color::BLACK, text: Default::default(), - sliders: Default::default(), - text_input: Default::default(), } } @@ -49,9 +43,7 @@ impl Program for Controls { Command::none() } - fn view(&mut self) -> Element<Message, Renderer> { - let [r, g, b] = &mut self.sliders; - let t = &mut self.text_input; + fn view(&self) -> Element<Message, Renderer> { let background_color = self.background_color; let text = &self.text; @@ -59,7 +51,7 @@ impl Program for Controls { .width(Length::Units(500)) .spacing(20) .push( - Slider::new(r, 0.0..=1.0, background_color.r, move |r| { + slider(0.0..=1.0, background_color.r, move |r| { Message::BackgroundColorChanged(Color { r, ..background_color @@ -68,7 +60,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(g, 0.0..=1.0, background_color.g, move |g| { + slider(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color { g, ..background_color @@ -77,7 +69,7 @@ impl Program for Controls { .step(0.01), ) .push( - Slider::new(b, 0.0..=1.0, background_color.b, move |b| { + slider(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color { b, ..background_color @@ -100,19 +92,18 @@ impl Program for Controls { .spacing(10) .push( Text::new("Background color") - .color(Color::WHITE), + .style(Color::WHITE), ) .push(sliders) .push( Text::new(format!("{:?}", background_color)) .size(14) - .color(Color::WHITE), + .style(Color::WHITE), ) - .push(TextInput::new( - t, + .push(text_input( "Placeholder", text, - move |text| Message::TextChanged(text), + Message::TextChanged, )), ), ) diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration_wgpu/src/main.rs index 045ee0d3..86a0d6a4 100644 --- a/examples/integration_wgpu/src/main.rs +++ b/examples/integration_wgpu/src/main.rs @@ -5,9 +5,11 @@ use controls::Controls; use scene::Scene; use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{conversion, futures, program, winit, Clipboard, Debug, Size}; +use iced_winit::{ + conversion, futures, program, renderer, winit, Clipboard, Color, Debug, + Size, +}; -use futures::task::SpawnExt; use winit::{ dpi::PhysicalPosition, event::{Event, ModifiersState, WindowEvent}, @@ -71,7 +73,7 @@ pub fn main() { let instance = wgpu::Instance::new(backend); let surface = unsafe { instance.create_surface(&window) }; - let (format, (mut device, queue)) = futures::executor::block_on(async { + let (format, (device, queue)) = futures::executor::block_on(async { let adapter = wgpu::util::initialize_adapter_from_env_or_default( &instance, backend, @@ -91,7 +93,9 @@ pub fn main() { ( surface - .get_preferred_format(&adapter) + .get_supported_formats(&adapter) + .first() + .copied() .expect("Get preferred format"), adapter .request_device( @@ -114,24 +118,23 @@ pub fn main() { format, width: physical_size.width, height: physical_size.height, - present_mode: wgpu::PresentMode::Mailbox, + present_mode: wgpu::PresentMode::AutoVsync, }, ); let mut resized = false; - // Initialize staging belt and local pool + // Initialize staging belt let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); - let mut local_pool = futures::executor::LocalPool::new(); // Initialize scene and GUI controls - let scene = Scene::new(&mut device, format); + let scene = Scene::new(&device, format); let controls = Controls::new(); // Initialize iced let mut debug = Debug::new(); let mut renderer = - Renderer::new(Backend::new(&mut device, Settings::default(), format)); + Renderer::new(Backend::new(&device, Settings::default(), format)); let mut state = program::State::new( controls, @@ -188,6 +191,8 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, + &iced_wgpu::Theme::Dark, + &renderer::Style { text_color: Color::WHITE }, &mut clipboard, &mut debug, ); @@ -203,11 +208,11 @@ pub fn main() { surface.configure( &device, &wgpu::SurfaceConfiguration { + format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: format, width: size.width, height: size.height, - present_mode: wgpu::PresentMode::Mailbox, + present_mode: wgpu::PresentMode::AutoVsync, }, ); @@ -239,7 +244,7 @@ pub fn main() { // And then iced on top renderer.with_primitives(|backend, primitive| { backend.present( - &mut device, + &device, &mut staging_belt, &mut encoder, &view, @@ -262,12 +267,8 @@ pub fn main() { ); // And recall staging buffers - local_pool - .spawner() - .spawn(staging_belt.recall()) - .expect("Recall staging buffers"); + staging_belt.recall(); - local_pool.run_until_stalled(); } Err(error) => match error { wgpu::SurfaceError::OutOfMemory => { diff --git a/examples/integration_wgpu/src/scene.rs b/examples/integration_wgpu/src/scene.rs index fbda1326..3e41fbda 100644 --- a/examples/integration_wgpu/src/scene.rs +++ b/examples/integration_wgpu/src/scene.rs @@ -23,7 +23,7 @@ impl Scene { ) -> wgpu::RenderPass<'a> { encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, - color_attachments: &[wgpu::RenderPassColorAttachment { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, ops: wgpu::Operations { @@ -39,7 +39,7 @@ impl Scene { }), store: true, }, - }], + })], depth_stencil_attachment: None, }) } @@ -55,8 +55,8 @@ fn build_pipeline( texture_format: wgpu::TextureFormat, ) -> wgpu::RenderPipeline { let (vs_module, fs_module) = ( - device.create_shader_module(&wgpu::include_wgsl!("shader/vert.wgsl")), - device.create_shader_module(&wgpu::include_wgsl!("shader/frag.wgsl")), + device.create_shader_module(wgpu::include_wgsl!("shader/vert.wgsl")), + device.create_shader_module(wgpu::include_wgsl!("shader/frag.wgsl")), ); let pipeline_layout = @@ -66,40 +66,37 @@ fn build_pipeline( bind_group_layouts: &[], }); - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", - buffers: &[], - }, - fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", - targets: &[wgpu::ColorTargetState { - format: texture_format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - }], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Ccw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - - pipeline + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs_module, + entry_point: "main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fs_module, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Ccw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }) } diff --git a/examples/integration_wgpu/src/shader/frag.wgsl b/examples/integration_wgpu/src/shader/frag.wgsl index a6f61336..cf27bb56 100644 --- a/examples/integration_wgpu/src/shader/frag.wgsl +++ b/examples/integration_wgpu/src/shader/frag.wgsl @@ -1,4 +1,4 @@ -[[stage(fragment)]] -fn main() -> [[location(0)]] vec4<f32> { +@fragment +fn main() -> @location(0) vec4<f32> { return vec4<f32>(1.0, 0.0, 0.0, 1.0); } diff --git a/examples/integration_wgpu/src/shader/vert.wgsl b/examples/integration_wgpu/src/shader/vert.wgsl index 7ef47fb2..e353e6ba 100644 --- a/examples/integration_wgpu/src/shader/vert.wgsl +++ b/examples/integration_wgpu/src/shader/vert.wgsl @@ -1,5 +1,5 @@ -[[stage(vertex)]] -fn main([[builtin(vertex_index)]] in_vertex_index: u32) -> [[builtin(position)]] vec4<f32> { +@vertex +fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> { let x = f32(1 - i32(in_vertex_index)) * 0.5; let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5; return vec4<f32>(x, y, 0.0, 1.0); diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 2962ca25..ae8fa22b 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,14 +1,13 @@ use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; use iced::executor; use iced::keyboard; -use iced::pane_grid::{self, PaneGrid}; -use iced::scrollable::{self, Scrollable}; +use iced::theme::{self, Theme}; +use iced::widget::pane_grid::{self, PaneGrid}; +use iced::widget::{button, column, container, row, scrollable, text}; use iced::{ - Application, Color, Column, Command, Container, Element, Length, Row, - Settings, Size, Subscription, Text, + Application, Color, Command, Element, Length, Settings, Size, Subscription, }; -use iced_lazy::responsive::{self, Responsive}; +use iced_lazy::responsive; use iced_native::{event, subscription, Event}; pub fn main() -> iced::Result { @@ -36,6 +35,7 @@ enum Message { impl Application for Example { type Message = Message; + type Theme = Theme; type Executor = executor::Default; type Flags = (); @@ -153,57 +153,47 @@ impl Application for Example { }) } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let focus = self.focus; let total_panes = self.panes.len(); - let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| { + let pane_grid = PaneGrid::new(&self.panes, |id, pane| { let is_focused = focus == Some(id); - let Pane { - responsive, + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(Message::TogglePin(id)) + .padding(3); + + let title = row![ pin_button, - is_pinned, - content, - .. - } = pane; - - let text = if *is_pinned { "Unpin" } else { "Pin" }; - let pin_button = Button::new(pin_button, Text::new(text).size(14)) - .on_press(Message::TogglePin(id)) - .style(style::Button::Pin) - .padding(3); - - let title = Row::with_children(vec![ - pin_button.into(), - Text::new("Pane").into(), - Text::new(content.id.to_string()) - .color(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }) - .into(), - ]) + "Pane", + text(pane.id.to_string()).style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + ] .spacing(5); let title_bar = pane_grid::TitleBar::new(title) - .controls(pane.controls.view(id, total_panes, *is_pinned)) + .controls(view_controls(id, total_panes, pane.is_pinned)) .padding(10) .style(if is_focused { - style::TitleBar::Focused + style::title_bar_focused } else { - style::TitleBar::Active + style::title_bar_active }); - pane_grid::Content::new(Responsive::new(responsive, move |size| { - content.view(id, total_panes, *is_pinned, size) + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, pane.is_pinned, size) })) .title_bar(title_bar) .style(if is_focused { - style::Pane::Focused + style::pane_focused } else { - style::Pane::Active + style::pane_active }) }) .width(Length::Fill) @@ -213,7 +203,7 @@ impl Application for Example { .on_drag(Message::Dragged) .on_resize(10, Message::Resized); - Container::new(pane_grid) + container(pane_grid) .width(Length::Fill) .height(Length::Fill) .padding(10) @@ -253,247 +243,132 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { } struct Pane { - pub responsive: responsive::State, - pub is_pinned: bool, - pub pin_button: button::State, - pub content: Content, - pub controls: Controls, -} - -struct Content { id: usize, - scroll: scrollable::State, - split_horizontally: button::State, - split_vertically: button::State, - close: button::State, -} - -struct Controls { - close: button::State, + pub is_pinned: bool, } impl Pane { fn new(id: usize) -> Self { Self { - responsive: responsive::State::new(), + id, is_pinned: false, - pin_button: button::State::new(), - content: Content::new(id), - controls: Controls::new(), } } } -impl Content { - fn new(id: usize) -> Self { - Content { - id, - scroll: scrollable::State::new(), - split_horizontally: button::State::new(), - split_vertically: button::State::new(), - close: button::State::new(), - } +fn view_content<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, Message> { + let button = |label, message| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + }; + + let mut controls = column![ + button( + "Split horizontally", + Message::Split(pane_grid::Axis::Horizontal, pane), + ), + button( + "Split vertically", + Message::Split(pane_grid::Axis::Vertical, pane), + ) + ] + .spacing(5) + .max_width(150); + + if total_panes > 1 && !is_pinned { + controls = controls.push( + button("Close", Message::Close(pane)) + .style(theme::Button::Destructive), + ); } - fn view( - &mut self, - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - size: Size, - ) -> Element<Message> { - let Content { - scroll, - split_horizontally, - split_vertically, - close, - .. - } = self; - - let button = |state, label, message, style| { - Button::new( - state, - Text::new(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - .style(style) - }; - - let mut controls = Column::new() - .spacing(5) - .max_width(150) - .push(button( - split_horizontally, - "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), - style::Button::Primary, - )) - .push(button( - split_vertically, - "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), - style::Button::Primary, - )); - - if total_panes > 1 && !is_pinned { - controls = controls.push(button( - close, - "Close", - Message::Close(pane), - style::Button::Destructive, - )); - } - let content = Scrollable::new(scroll) - .width(Length::Fill) - .spacing(10) - .align_items(Alignment::Center) - .push(Text::new(format!("{}x{}", size.width, size.height)).size(24)) - .push(controls); + let content = column![ + text(format!("{}x{}", size.width, size.height)).size(24), + controls, + ] + .width(Length::Fill) + .spacing(10) + .align_items(Alignment::Center); - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() - } + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() } -impl Controls { - fn new() -> Self { - Self { - close: button::State::new(), - } +fn view_controls<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, +) -> Element<'a, Message> { + let mut button = button(text("Close").size(14)) + .style(theme::Button::Destructive) + .padding(3); + + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); } - pub fn view( - &mut self, - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - ) -> Element<Message> { - let mut button = - Button::new(&mut self.close, Text::new("Close").size(14)) - .style(style::Button::Control) - .padding(3); - if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); - } - button.into() - } + button.into() } mod style { - use crate::PANE_ID_COLOR_FOCUSED; - use iced::{button, container, Background, Color, Vector}; - - const SURFACE: Color = Color::from_rgb( - 0xF2 as f32 / 255.0, - 0xF3 as f32 / 255.0, - 0xF5 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub enum TitleBar { - Active, - Focused, - } + use iced::widget::container; + use iced::Theme; - impl container::StyleSheet for TitleBar { - fn style(&self) -> container::Style { - let pane = match self { - Self::Active => Pane::Active, - Self::Focused => Pane::Focused, - } - .style(); + pub fn title_bar_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - container::Style { - text_color: Some(Color::WHITE), - background: Some(pane.border_color.into()), - ..Default::default() - } + container::Appearance { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() } } - pub enum Pane { - Active, - Focused, - } + pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - impl container::StyleSheet for Pane { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(SURFACE)), - border_width: 2.0, - border_color: match self { - Self::Active => Color::from_rgb(0.7, 0.7, 0.7), - Self::Focused => Color::BLACK, - }, - ..Default::default() - } + container::Appearance { + text_color: Some(palette.primary.strong.text), + background: Some(palette.primary.strong.color.into()), + ..Default::default() } } - pub enum Button { - Primary, - Destructive, - Control, - Pin, - } + pub fn pane_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - let (background, text_color) = match self { - Button::Primary => (Some(ACTIVE), Color::WHITE), - Button::Destructive => { - (None, Color::from_rgb8(0xFF, 0x47, 0x47)) - } - Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE), - Button::Pin => (Some(ACTIVE), Color::WHITE), - }; - - button::Style { - text_color, - background: background.map(Background::Color), - border_radius: 5.0, - shadow_offset: Vector::new(0.0, 0.0), - ..button::Style::default() - } + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.background.strong.color, + ..Default::default() } + } - fn hovered(&self) -> button::Style { - let active = self.active(); + pub fn pane_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); - let background = match self { - Button::Primary => Some(HOVERED), - Button::Destructive => Some(Color { - a: 0.2, - ..active.text_color - }), - Button::Control => Some(PANE_ID_COLOR_FOCUSED), - Button::Pin => Some(HOVERED), - }; - - button::Style { - background: background.map(Background::Color), - ..active - } + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.primary.strong.color, + ..Default::default() } } } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 52303d70..9df1f5c7 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,7 +1,5 @@ -use iced::{ - pick_list, scrollable, Alignment, Container, Element, Length, PickList, - Sandbox, Scrollable, Settings, Space, Text, -}; +use iced::widget::{column, container, pick_list, scrollable, vertical_space}; +use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Example::run(Settings::default()) @@ -9,8 +7,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Example { - scroll: scrollable::State, - pick_list: pick_list::State<Language>, selected_language: Option<Language>, } @@ -38,26 +34,25 @@ impl Sandbox for Example { } } - fn view(&mut self) -> Element<Message> { - let pick_list = PickList::new( - &mut self.pick_list, + fn view(&self) -> Element<Message> { + let pick_list = pick_list( &Language::ALL[..], self.selected_language, Message::LanguageSelected, ) .placeholder("Choose a language..."); - let mut content = Scrollable::new(&mut self.scroll) - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(Space::with_height(Length::Units(600))) - .push(Text::new("Which is your favorite language?")) - .push(pick_list); - - content = content.push(Space::with_height(Length::Units(600))); + let content = column![ + vertical_space(Length::Units(600)), + "Which is your favorite language?", + pick_list, + vertical_space(Length::Units(600)), + ] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10); - Container::new(content) + container(scrollable(content)) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 85c26987..4fe2d07c 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,7 @@ +use iced::futures; +use iced::widget::{self, column, container, image, row, text}; use iced::{ - button, futures, image, Alignment, Application, Button, Column, Command, - Container, Element, Length, Row, Settings, Text, + Alignment, Application, Color, Command, Element, Length, Settings, Theme, }; pub fn main() -> iced::Result { @@ -10,13 +11,8 @@ pub fn main() -> iced::Result { #[derive(Debug)] enum Pokedex { Loading, - Loaded { - pokemon: Pokemon, - search: button::State, - }, - Errored { - try_again: button::State, - }, + Loaded { pokemon: Pokemon }, + Errored, } #[derive(Debug, Clone)] @@ -26,8 +22,9 @@ enum Message { } impl Application for Pokedex { - type Executor = iced::executor::Default; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Pokedex, Command<Message>) { @@ -50,17 +47,12 @@ impl Application for Pokedex { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::PokemonFound(Ok(pokemon)) => { - *self = Pokedex::Loaded { - pokemon, - search: button::State::new(), - }; + *self = Pokedex::Loaded { pokemon }; Command::none() } Message::PokemonFound(Err(_error)) => { - *self = Pokedex::Errored { - try_again: button::State::new(), - }; + *self = Pokedex::Errored; Command::none() } @@ -75,27 +67,28 @@ impl Application for Pokedex { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let content = match self { - Pokedex::Loading => Column::new() - .width(Length::Shrink) - .push(Text::new("Searching for Pokémon...").size(40)), - Pokedex::Loaded { pokemon, search } => Column::new() - .max_width(500) - .spacing(20) - .align_items(Alignment::End) - .push(pokemon.view()) - .push( - button(search, "Keep searching!").on_press(Message::Search), - ), - Pokedex::Errored { try_again, .. } => Column::new() - .spacing(20) - .align_items(Alignment::End) - .push(Text::new("Whoops! Something went wrong...").size(40)) - .push(button(try_again, "Try again").on_press(Message::Search)), + Pokedex::Loading => { + column![text("Searching for Pokémon...").size(40),] + .width(Length::Shrink) + } + Pokedex::Loaded { pokemon } => column![ + pokemon.view(), + button("Keep searching!").on_press(Message::Search) + ] + .max_width(500) + .spacing(20) + .align_items(Alignment::End), + Pokedex::Errored => column![ + text("Whoops! Something went wrong...").size(40), + button("Try again").on_press(Message::Search) + ] + .spacing(20) + .align_items(Alignment::End), }; - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -110,41 +103,30 @@ struct Pokemon { name: String, description: String, image: image::Handle, - image_viewer: image::viewer::State, } impl Pokemon { const TOTAL: u16 = 807; - fn view(&mut self) -> Element<Message> { - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(image::Viewer::new( - &mut self.image_viewer, - self.image.clone(), - )) - .push( - Column::new() - .spacing(20) - .push( - Row::new() - .align_items(Alignment::Center) - .spacing(20) - .push( - Text::new(&self.name) - .size(30) - .width(Length::Fill), - ) - .push( - Text::new(format!("#{}", self.number)) - .size(20) - .color([0.5, 0.5, 0.5]), - ), - ) - .push(Text::new(&self.description)), - ) - .into() + fn view(&self) -> Element<Message> { + row![ + image::viewer(self.image.clone()), + column![ + row![ + text(&self.name).size(30).width(Length::Fill), + text(format!("#{}", self.number)) + .size(20) + .style(Color::from([0.5, 0.5, 0.5])), + ] + .align_items(Alignment::Center) + .spacing(20), + self.description.as_ref(), + ] + .spacing(20), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } async fn search() -> Result<Pokemon, Error> { @@ -188,8 +170,7 @@ impl Pokemon { let description = entry .flavor_text_entries .iter() - .filter(|text| text.language.name == "en") - .next() + .find(|text| text.language.name == "en") .ok_or(Error::LanguageError)?; Ok(Pokemon { @@ -201,7 +182,6 @@ impl Pokemon { .map(|c| if c.is_control() { ' ' } else { c }) .collect(), image, - image_viewer: image::viewer::State::new(), }) } @@ -237,30 +217,6 @@ impl From<reqwest::Error> for Error { } } -fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { - Button::new(state, Text::new(text)) - .padding(10) - .style(style::Button::Primary) -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } +fn button(text: &str) -> widget::Button<'_, Message> { + widget::button(text).padding(10) } diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index c9a8e798..d4ebe4d3 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -1,4 +1,5 @@ -use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; +use iced::widget::{column, progress_bar, slider}; +use iced::{Element, Sandbox, Settings}; pub fn main() -> iced::Result { Progress::run(Settings::default()) @@ -7,7 +8,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Progress { value: f32, - progress_bar_slider: slider::State, } #[derive(Debug, Clone, Copy)] @@ -32,19 +32,12 @@ impl Sandbox for Progress { } } - fn view(&mut self) -> Element<Message> { - Column::new() - .padding(20) - .push(ProgressBar::new(0.0..=100.0, self.value)) - .push( - Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - ) - .step(0.01), - ) - .into() + fn view(&self) -> Element<Message> { + column![ + progress_bar(0.0..=100.0, self.value), + slider(0.0..=100.0, self.value, Message::SliderChanged).step(0.01) + ] + .padding(20) + .into() } } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 92c82d45..6f487e4c 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,8 +1,6 @@ -use iced::qr_code::{self, QRCode}; -use iced::text_input::{self, TextInput}; -use iced::{ - Alignment, Column, Container, Element, Length, Sandbox, Settings, Text, -}; +use iced::widget::qr_code::{self, QRCode}; +use iced::widget::{column, container, text, text_input}; +use iced::{Alignment, Color, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { QRGenerator::run(Settings::default()) @@ -11,7 +9,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct QRGenerator { data: String, - input: text_input::State, qr_code: Option<qr_code::State>, } @@ -45,13 +42,12 @@ impl Sandbox for QRGenerator { } } - fn view(&mut self) -> Element<Message> { - let title = Text::new("QR Code Generator") + fn view(&self) -> Element<Message> { + let title = text("QR Code Generator") .size(70) - .color([0.5, 0.5, 0.5]); + .style(Color::from([0.5, 0.5, 0.5])); - let input = TextInput::new( - &mut self.input, + let input = text_input( "Type the data of your QR code here...", &self.data, Message::DataChanged, @@ -59,18 +55,16 @@ impl Sandbox for QRGenerator { .size(30) .padding(15); - let mut content = Column::new() + let mut content = column![title, input] .width(Length::Units(700)) .spacing(20) - .align_items(Alignment::Center) - .push(title) - .push(input); + .align_items(Alignment::Center); - if let Some(qr_code) = self.qr_code.as_mut() { + if let Some(qr_code) = self.qr_code.as_ref() { content = content.push(QRCode::new(qr_code).cell_size(10)); } - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 8e027504..b7b3dedc 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,176 +1,182 @@ -mod style; - -use iced::{ - button, scrollable, Button, Column, Container, Element, Length, - ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text, +use iced::executor; +use iced::widget::{ + button, column, container, horizontal_rule, progress_bar, radio, + scrollable, text, vertical_space, Row, }; +use iced::{Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) } struct ScrollableDemo { - theme: style::Theme, + theme: Theme, variants: Vec<Variant>, } #[derive(Debug, Clone)] enum Message { - ThemeChanged(style::Theme), + ThemeChanged(Theme), ScrollToTop(usize), ScrollToBottom(usize), Scrolled(usize, f32), } -impl Sandbox for ScrollableDemo { +impl Application for ScrollableDemo { type Message = Message; - - fn new() -> Self { - ScrollableDemo { - theme: Default::default(), - variants: Variant::all(), - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + ScrollableDemo { + theme: Default::default(), + variants: Variant::all(), + }, + Command::none(), + ) } fn title(&self) -> String { String::from("Scrollable - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::ThemeChanged(theme) => self.theme = theme, + Message::ThemeChanged(theme) => { + self.theme = theme; + + Command::none() + } Message::ScrollToTop(i) => { if let Some(variant) = self.variants.get_mut(i) { - variant.scrollable.snap_to(0.0); - variant.latest_offset = 0.0; + + scrollable::snap_to(Variant::id(i), 0.0) + } else { + Command::none() } } Message::ScrollToBottom(i) => { if let Some(variant) = self.variants.get_mut(i) { - variant.scrollable.snap_to(1.0); - variant.latest_offset = 1.0; + + scrollable::snap_to(Variant::id(i), 1.0) + } else { + Command::none() } } Message::Scrolled(i, offset) => { if let Some(variant) = self.variants.get_mut(i) { variant.latest_offset = offset; } + + Command::none() } } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let ScrollableDemo { theme, variants, .. } = self; - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + column!["Choose a theme:"].spacing(10), |column, option| { - column.push( - Radio::new( - *option, - format!("{:?}", option), - Some(*theme), - Message::ThemeChanged, - ) - .style(*theme), - ) + column.push(radio( + format!("{:?}", option), + *option, + Some(*theme), + Message::ThemeChanged, + )) }, ); let scrollable_row = Row::with_children( variants - .iter_mut() + .iter() .enumerate() .map(|(i, variant)| { - let mut scrollable = - Scrollable::new(&mut variant.scrollable) - .padding(10) - .spacing(10) + let mut contents = column![ + variant.title, + button("Scroll to bottom",) .width(Length::Fill) - .height(Length::Fill) - .on_scroll(move |offset| { - Message::Scrolled(i, offset) - }) - .style(*theme) - .push(Text::new(variant.title)) - .push( - Button::new( - &mut variant.scroll_to_bottom, - Text::new("Scroll to bottom"), - ) - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToBottom(i)), - ); + .padding(10) + .on_press(Message::ScrollToBottom(i)), + ] + .padding(10) + .spacing(10) + .width(Length::Fill); if let Some(scrollbar_width) = variant.scrollbar_width { - scrollable = scrollable - .scrollbar_width(scrollbar_width) - .push(Text::new(format!( - "scrollbar_width: {:?}", - scrollbar_width - ))); + contents = contents.push(text(format!( + "scrollbar_width: {:?}", + scrollbar_width + ))); } if let Some(scrollbar_margin) = variant.scrollbar_margin { - scrollable = scrollable - .scrollbar_margin(scrollbar_margin) - .push(Text::new(format!( - "scrollbar_margin: {:?}", - scrollbar_margin - ))); + contents = contents.push(text(format!( + "scrollbar_margin: {:?}", + scrollbar_margin + ))); } if let Some(scroller_width) = variant.scroller_width { - scrollable = scrollable - .scroller_width(scroller_width) - .push(Text::new(format!( - "scroller_width: {:?}", - scroller_width - ))); + contents = contents.push(text(format!( + "scroller_width: {:?}", + scroller_width + ))); } - scrollable = scrollable - .push(Space::with_height(Length::Units(100))) - .push(Text::new( + contents = contents + .push(vertical_space(Length::Units(100))) + .push( "Some content that should wrap within the \ scrollable. Let's output a lot of short words, so \ that we'll make sure to see how wrapping works \ with these scrollbars.", - )) - .push(Space::with_height(Length::Units(1200))) - .push(Text::new("Middle")) - .push(Space::with_height(Length::Units(1200))) - .push(Text::new("The End.")) + ) + .push(vertical_space(Length::Units(1200))) + .push("Middle") + .push(vertical_space(Length::Units(1200))) + .push("The End.") .push( - Button::new( - &mut variant.scroll_to_top, - Text::new("Scroll to top"), - ) - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToTop(i)), + button("Scroll to top") + .width(Length::Fill) + .padding(10) + .on_press(Message::ScrollToTop(i)), ); - Column::new() - .width(Length::Fill) + let mut scrollable = scrollable(contents) + .id(Variant::id(i)) .height(Length::Fill) - .spacing(10) - .push( - Container::new(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .style(*theme), - ) - .push(ProgressBar::new( - 0.0..=1.0, - variant.latest_offset, - )) - .into() + .on_scroll(move |offset| Message::Scrolled(i, offset)); + + if let Some(scrollbar_width) = variant.scrollbar_width { + scrollable = + scrollable.scrollbar_width(scrollbar_width); + } + + if let Some(scrollbar_margin) = variant.scrollbar_margin { + scrollable = + scrollable.scrollbar_margin(scrollbar_margin); + } + + if let Some(scroller_width) = variant.scroller_width { + scrollable = scrollable.scroller_width(scroller_width); + } + + column![ + scrollable, + progress_bar(0.0..=1.0, variant.latest_offset,) + ] + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .into() }) .collect(), ) @@ -178,29 +184,27 @@ impl Sandbox for ScrollableDemo { .width(Length::Fill) .height(Length::Fill); - let content = Column::new() - .spacing(20) - .padding(20) - .push(choose_theme) - .push(Rule::horizontal(20).style(self.theme)) - .push(scrollable_row); + let content = + column![choose_theme, horizontal_rule(20), scrollable_row] + .spacing(20) + .padding(20); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .style(self.theme) .into() } + + fn theme(&self) -> Theme { + self.theme + } } /// A version of a scrollable struct Variant { title: &'static str, - scrollable: scrollable::State, - scroll_to_top: button::State, - scroll_to_bottom: button::State, scrollbar_width: Option<u16>, scrollbar_margin: Option<u16>, scroller_width: Option<u16>, @@ -212,9 +216,6 @@ impl Variant { vec![ Self { title: "Default Scrollbar", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: None, scrollbar_margin: None, scroller_width: None, @@ -222,9 +223,6 @@ impl Variant { }, Self { title: "Slimmed & Margin", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: Some(3), scroller_width: Some(4), @@ -232,9 +230,6 @@ impl Variant { }, Self { title: "Wide Scroller", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(4), scrollbar_margin: None, scroller_width: Some(10), @@ -242,9 +237,6 @@ impl Variant { }, Self { title: "Narrow Scroller", - scrollable: scrollable::State::new(), - scroll_to_top: button::State::new(), - scroll_to_bottom: button::State::new(), scrollbar_width: Some(10), scrollbar_margin: None, scroller_width: Some(4), @@ -252,4 +244,8 @@ impl Variant { }, ] } + + pub fn id(i: usize) -> scrollable::Id { + scrollable::Id::new(format!("scrollable-{}", i)) + } } diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs deleted file mode 100644 index 0ed38b00..00000000 --- a/examples/scrollable/src/style.rs +++ /dev/null @@ -1,191 +0,0 @@ -use iced::{container, radio, rule, scrollable}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Theme { - Light, - Dark, -} - -impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; -} - -impl Default for Theme { - fn default() -> Theme { - Theme::Light - } -} - -impl<'a> From<Theme> for Box<dyn container::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } -} - -impl<'a> From<Theme> for Box<dyn radio::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } -} - -impl<'a> From<Theme> for Box<dyn scrollable::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } -} - -impl From<Theme> for Box<dyn rule::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Rule.into(), - } - } -} - -mod dark { - use iced::{container, radio, rule, scrollable, Color}; - - const BACKGROUND: Color = Color::from_rgb( - 0x36 as f32 / 255.0, - 0x39 as f32 / 255.0, - 0x3F as f32 / 255.0, - ); - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const SCROLLBAR: Color = Color::from_rgb( - 0x2E as f32 / 255.0, - 0x33 as f32 / 255.0, - 0x38 as f32 / 255.0, - ); - - const SCROLLER: Color = Color::from_rgb( - 0x20 as f32 / 255.0, - 0x22 as f32 / 255.0, - 0x25 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Color { - a: 0.99, - ..BACKGROUND - } - .into(), - text_color: Color::WHITE.into(), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: SURFACE.into(), - dot_color: ACTIVE, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Color { a: 0.5, ..SURFACE }.into(), - ..self.active() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: Color { - a: 0.8, - ..SCROLLBAR - } - .into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: Color { a: 0.7, ..SCROLLER }, - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: SCROLLBAR.into(), - scroller: scrollable::Scroller { - color: SCROLLER, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: ACCENT, - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Rule; - - impl rule::StyleSheet for Rule { - fn style(&self) -> rule::Style { - rule::Style { - color: SURFACE, - width: 2, - radius: 1.0, - fill_mode: rule::FillMode::Percent(30.0), - } - } - } -} diff --git a/examples/sierpinski_triangle/Cargo.toml b/examples/sierpinski_triangle/Cargo.toml new file mode 100644 index 00000000..39d45f64 --- /dev/null +++ b/examples/sierpinski_triangle/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sierpinski_triangle" +version = "0.1.0" +authors = ["xkenmon <xkenmon@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "debug"] } +rand = "0.8.4" diff --git a/examples/sierpinski_triangle/README.md b/examples/sierpinski_triangle/README.md new file mode 100644 index 00000000..9fd18257 --- /dev/null +++ b/examples/sierpinski_triangle/README.md @@ -0,0 +1,16 @@ +## Sierpinski Triangle Emulator + +A simple [Sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use canvas and slider. + +Left-click add fixed point, right-click remove fixed point. + +<div align="center"> + <a href="https://gfycat.com/flippantrectangularechidna"> + <img src="https://thumbs.gfycat.com/FlippantRectangularEchidna-size_restricted.gif"> + </a> +</div> + +You can run with cargo: +``` +cargo run --package sierpinski_triangle +```
\ No newline at end of file diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs new file mode 100644 index 00000000..1d25d171 --- /dev/null +++ b/examples/sierpinski_triangle/src/main.rs @@ -0,0 +1,193 @@ +use std::fmt::Debug; + +use iced::executor; +use iced::widget::canvas::event::{self, Event}; +use iced::widget::canvas::{self, Canvas}; +use iced::widget::{column, row, slider, text}; +use iced::{ + Application, Color, Command, Length, Point, Rectangle, Settings, Size, + Theme, +}; + +use rand::Rng; + +fn main() -> iced::Result { + SierpinskiEmulator::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Debug)] +struct SierpinskiEmulator { + graph: SierpinskiGraph, +} + +#[derive(Debug, Clone)] +pub enum Message { + IterationSet(i32), + PointAdded(Point), + PointRemoved, +} + +impl Application for SierpinskiEmulator { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, iced::Command<Self::Message>) { + let emulator = SierpinskiEmulator { + graph: SierpinskiGraph::new(), + }; + (emulator, Command::none()) + } + + fn title(&self) -> String { + "Sierpinski Triangle Emulator".to_string() + } + + fn update( + &mut self, + message: Self::Message, + ) -> iced::Command<Self::Message> { + match message { + Message::IterationSet(cur_iter) => { + self.graph.iteration = cur_iter; + } + Message::PointAdded(point) => { + self.graph.fix_points.push(point); + self.graph.random_points.clear(); + } + Message::PointRemoved => { + self.graph.fix_points.pop(); + self.graph.random_points.clear(); + } + } + + self.graph.redraw(); + + Command::none() + } + + fn view(&self) -> iced::Element<'_, Self::Message> { + column![ + Canvas::new(&self.graph) + .width(Length::Fill) + .height(Length::Fill), + 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() + } +} + +#[derive(Default, Debug)] +struct SierpinskiGraph { + iteration: i32, + fix_points: Vec<Point>, + random_points: Vec<Point>, + cache: canvas::Cache, +} + +impl canvas::Program<Message> for SierpinskiGraph { + type State = (); + + fn update( + &self, + _state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: canvas::Cursor, + ) -> (event::Status, Option<Message>) { + let cursor_position = + if let Some(position) = cursor.position_in(&bounds) { + position + } else { + return (event::Status::Ignored, None); + }; + + match event { + Event::Mouse(mouse_event) => { + let message = match mouse_event { + iced::mouse::Event::ButtonPressed( + iced::mouse::Button::Left, + ) => Some(Message::PointAdded(cursor_position)), + iced::mouse::Event::ButtonPressed( + iced::mouse::Button::Right, + ) => Some(Message::PointRemoved), + _ => None, + }; + (event::Status::Captured, message) + } + _ => (event::Status::Ignored, None), + } + } + + fn draw( + &self, + _state: &Self::State, + _theme: &Theme, + bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> Vec<canvas::Geometry> { + let geom = self.cache.draw(bounds.size(), |frame| { + frame.stroke( + &canvas::Path::rectangle(Point::ORIGIN, frame.size()), + canvas::Stroke::default(), + ); + + if self.fix_points.is_empty() { + return; + } + + let mut last = None; + + for _ in 0..self.iteration { + let p = self.gen_rand_point(last); + let path = canvas::Path::rectangle(p, Size::new(1_f32, 1_f32)); + + frame.stroke(&path, canvas::Stroke::default()); + + last = Some(p); + } + + self.fix_points.iter().for_each(|p| { + let path = canvas::Path::circle(*p, 5.0); + frame.fill(&path, Color::from_rgb8(0x12, 0x93, 0xD8)); + }); + }); + + vec![geom] + } +} + +impl SierpinskiGraph { + fn new() -> SierpinskiGraph { + SierpinskiGraph::default() + } + + fn redraw(&mut self) { + self.cache.clear(); + } + + fn gen_rand_point(&self, last: Option<Point>) -> Point { + let dest_point_idx = + rand::thread_rng().gen_range(0..self.fix_points.len()); + + let dest_point = self.fix_points[dest_point_idx]; + let cur_point = last.or_else(|| Some(self.fix_points[0])).unwrap(); + + Point::new( + (dest_point.x + cur_point.x) / 2_f32, + (dest_point.y + cur_point.y) / 2_f32, + ) + } +} diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 12184dd1..c59d73a8 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -6,10 +6,16 @@ //! Inspired by the example found in the MDN docs[1]. //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system +use iced::application; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::time; +use iced::widget::canvas; +use iced::widget::canvas::{Cursor, Path, Stroke}; +use iced::window; use iced::{ - canvas::{self, Cursor, Path, Stroke}, - executor, time, window, Application, Canvas, Color, Command, Element, - Length, Point, Rectangle, Settings, Size, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Settings, + Size, Subscription, Vector, }; use std::time::Instant; @@ -31,8 +37,9 @@ enum Message { } impl Application for SolarSystem { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command<Message>) { @@ -59,16 +66,26 @@ impl Application for SolarSystem { } fn subscription(&self) -> Subscription<Message> { - time::every(std::time::Duration::from_millis(10)) - .map(|instant| Message::Tick(instant)) + time::every(std::time::Duration::from_millis(10)).map(Message::Tick) } - fn view(&mut self) -> Element<Message> { - Canvas::new(&mut self.state) + fn view(&self) -> Element<Message> { + canvas(&self.state) .width(Length::Fill) .height(Length::Fill) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } + + fn style(&self) -> theme::Application { + theme::Application::Custom(|_theme| application::Appearance { + background_color: Color::BLACK, + text_color: Color::WHITE, + }) + } } #[derive(Debug)] @@ -129,24 +146,24 @@ impl State { } impl<Message> canvas::Program<Message> for State { + type State = (); + fn draw( &self, + _state: &Self::State, + _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; let background = self.space_cache.draw(bounds.size(), |frame| { - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let stars = Path::new(|path| { for (p, size) in &self.stars { path.rectangle(*p, Size::new(*size, *size)); } }); - frame.fill(&space, Color::BLACK); - frame.translate(frame.center() - Point::ORIGIN); frame.fill(&stars, Color::WHITE); }); diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index dc8a4de7..b8cee807 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,7 +1,12 @@ +use iced::alignment; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::time; +use iced::widget::{button, column, container, row, text}; use iced::{ - alignment, button, executor, time, Alignment, Application, Button, Column, - Command, Container, Element, Length, Row, Settings, Subscription, Text, + Alignment, Application, Command, Element, Length, Settings, Subscription, }; + use std::time::{Duration, Instant}; pub fn main() -> iced::Result { @@ -11,8 +16,6 @@ pub fn main() -> iced::Result { struct Stopwatch { duration: Duration, state: State, - toggle: button::State, - reset: button::State, } enum State { @@ -28,8 +31,9 @@ enum Message { } impl Application for Stopwatch { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Stopwatch, Command<Message>) { @@ -37,8 +41,6 @@ impl Application for Stopwatch { Stopwatch { duration: Duration::default(), state: State::Idle, - toggle: button::State::new(), - reset: button::State::new(), }, Command::none(), ) @@ -60,13 +62,12 @@ impl Application for Stopwatch { self.state = State::Idle; } }, - Message::Tick(now) => match &mut self.state { - State::Ticking { last_tick } => { + Message::Tick(now) => { + if let State::Ticking { last_tick } = &mut self.state { self.duration += now - *last_tick; *last_tick = now; } - _ => {} - }, + } Message::Reset => { self.duration = Duration::default(); } @@ -84,13 +85,13 @@ impl Application for Stopwatch { } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { const MINUTE: u64 = 60; const HOUR: u64 = 60 * MINUTE; let seconds = self.duration.as_secs(); - let duration = Text::new(format!( + let duration = text(format!( "{:0>2}:{:0>2}:{:0>2}.{:0>2}", seconds / HOUR, (seconds % HOUR) / MINUTE, @@ -99,42 +100,34 @@ impl Application for Stopwatch { )) .size(40); - let button = |state, label, style| { - Button::new( - state, - Text::new(label) - .horizontal_alignment(alignment::Horizontal::Center), + let button = |label| { + button( + text(label).horizontal_alignment(alignment::Horizontal::Center), ) - .min_width(80) .padding(10) - .style(style) + .width(Length::Units(80)) }; let toggle_button = { - let (label, color) = match self.state { - State::Idle => ("Start", style::Button::Primary), - State::Ticking { .. } => ("Stop", style::Button::Destructive), + let label = match self.state { + State::Idle => "Start", + State::Ticking { .. } => "Stop", }; - button(&mut self.toggle, label, color).on_press(Message::Toggle) + button(label).on_press(Message::Toggle) }; - let reset_button = - button(&mut self.reset, "Reset", style::Button::Secondary) - .on_press(Message::Reset); + let reset_button = button("Reset") + .style(theme::Button::Destructive) + .on_press(Message::Reset); - let controls = Row::new() - .spacing(20) - .push(toggle_button) - .push(reset_button); + let controls = row![toggle_button, reset_button].spacing(20); - let content = Column::new() + let content = column![duration, controls] .align_items(Alignment::Center) - .spacing(20) - .push(duration) - .push(controls); + .spacing(20); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -142,29 +135,3 @@ impl Application for Stopwatch { .into() } } - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index b4ef3e87..cda53e87 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,8 +1,9 @@ -use iced::{ - button, scrollable, slider, text_input, Alignment, Button, Checkbox, - Column, Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox, - Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, +use iced::widget::{ + button, checkbox, column, container, horizontal_rule, progress_bar, radio, + row, scrollable, slider, text, text_input, toggler, vertical_rule, + vertical_space, }; +use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; pub fn main() -> iced::Result { Styling::run(Settings::default()) @@ -10,12 +11,8 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Styling { - theme: style::Theme, - scroll: scrollable::State, - input: text_input::State, + theme: Theme, input_value: String, - button: button::State, - slider: slider::State, slider_value: f32, checkbox_value: bool, toggler_value: bool, @@ -23,7 +20,7 @@ struct Styling { #[derive(Debug, Clone)] enum Message { - ThemeChanged(style::Theme), + ThemeChanged(Theme), InputChanged(String), ButtonPressed, SliderChanged(f32), @@ -53,541 +50,88 @@ impl Sandbox for Styling { } } - fn view(&mut self) -> Element<Message> { - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), + fn view(&self) -> Element<Message> { + let choose_theme = [Theme::Light, Theme::Dark].iter().fold( + column![text("Choose a theme:")].spacing(10), |column, theme| { - column.push( - Radio::new( - *theme, - format!("{:?}", theme), - Some(self.theme), - Message::ThemeChanged, - ) - .style(self.theme), - ) + column.push(radio( + format!("{:?}", theme), + *theme, + Some(self.theme), + Message::ThemeChanged, + )) }, ); - let text_input = TextInput::new( - &mut self.input, + let text_input = text_input( "Type something...", &self.input_value, Message::InputChanged, ) .padding(10) - .size(20) - .style(self.theme); + .size(20); - let button = Button::new(&mut self.button, Text::new("Submit")) + let button = button("Submit") .padding(10) - .on_press(Message::ButtonPressed) - .style(self.theme); + .on_press(Message::ButtonPressed); - let slider = Slider::new( - &mut self.slider, - 0.0..=100.0, - self.slider_value, - Message::SliderChanged, - ) - .style(self.theme); + let slider = + slider(0.0..=100.0, self.slider_value, Message::SliderChanged); - let progress_bar = - ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); + let progress_bar = progress_bar(0.0..=100.0, self.slider_value); - let scrollable = Scrollable::new(&mut self.scroll) - .width(Length::Fill) - .height(Length::Units(100)) - .style(self.theme) - .push(Text::new("Scroll me!")) - .push(Space::with_height(Length::Units(800))) - .push(Text::new("You did it!")); + let scrollable = scrollable( + column![ + "Scroll me!", + vertical_space(Length::Units(800)), + "You did it!" + ] + .width(Length::Fill), + ) + .height(Length::Units(100)); - let checkbox = Checkbox::new( - self.checkbox_value, + let checkbox = checkbox( "Check me!", + self.checkbox_value, Message::CheckboxToggled, - ) - .style(self.theme); + ); - let toggler = Toggler::new( - self.toggler_value, + let toggler = toggler( String::from("Toggle me!"), + self.toggler_value, Message::TogglerToggled, ) .width(Length::Shrink) - .spacing(10) - .style(self.theme); - - let content = Column::new() - .spacing(20) - .padding(20) - .max_width(600) - .push(choose_theme) - .push(Rule::horizontal(38).style(self.theme)) - .push(Row::new().spacing(10).push(text_input).push(button)) - .push(slider) - .push(progress_bar) - .push( - Row::new() - .spacing(10) - .height(Length::Units(100)) - .align_items(Alignment::Center) - .push(scrollable) - .push(Rule::vertical(38).style(self.theme)) - .push( - Column::new() - .width(Length::Shrink) - .spacing(20) - .push(checkbox) - .push(toggler), - ), - ); + .spacing(10); + + let content = column![ + choose_theme, + horizontal_rule(38), + row![text_input, button].spacing(10), + slider, + progress_bar, + row![ + scrollable, + vertical_rule(38), + column![checkbox, toggler].spacing(20) + ] + .spacing(10) + .height(Length::Units(100)) + .align_items(Alignment::Center), + ] + .spacing(20) + .padding(20) + .max_width(600); - Container::new(content) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .style(self.theme) .into() } -} - -mod style { - use iced::{ - button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, toggler, - }; - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - Light, - Dark, - } - - impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; - } - - impl Default for Theme { - fn default() -> Theme { - Theme::Light - } - } - - impl<'a> From<Theme> for Box<dyn container::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn radio::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn text_input::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::TextInput.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn button::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => light::Button.into(), - Theme::Dark => dark::Button.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn scrollable::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn slider::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Slider.into(), - } - } - } - - impl From<Theme> for Box<dyn progress_bar::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::ProgressBar.into(), - } - } - } - - impl<'a> From<Theme> for Box<dyn checkbox::StyleSheet + 'a> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Checkbox.into(), - } - } - } - - impl From<Theme> for Box<dyn toggler::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Toggler.into(), - } - } - } - - impl From<Theme> for Box<dyn rule::StyleSheet> { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Rule.into(), - } - } - } - - mod light { - use iced::{button, Color, Vector}; - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Color::from_rgb(0.11, 0.42, 0.87).into(), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } - } - mod dark { - use iced::{ - button, checkbox, container, progress_bar, radio, rule, scrollable, - slider, text_input, toggler, Color, - }; - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Color::from_rgb8(0x36, 0x39, 0x3F).into(), - text_color: Color::WHITE.into(), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: SURFACE.into(), - dot_color: ACTIVE, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Color { a: 0.5, ..SURFACE }.into(), - ..self.active() - } - } - } - - pub struct TextInput; - - impl text_input::StyleSheet for TextInput { - fn active(&self) -> text_input::Style { - text_input::Style { - background: SURFACE.into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - - fn focused(&self) -> text_input::Style { - text_input::Style { - border_width: 1.0, - border_color: ACCENT, - ..self.active() - } - } - - fn hovered(&self) -> text_input::Style { - text_input::Style { - border_width: 1.0, - border_color: Color { a: 0.3, ..ACCENT }, - ..self.focused() - } - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.4, 0.4, 0.4) - } - - fn value_color(&self) -> Color { - Color::WHITE - } - - fn selection_color(&self) -> Color { - ACTIVE - } - } - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: ACTIVE.into(), - border_radius: 3.0, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: HOVERED.into(), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1.0, - border_color: Color::WHITE, - ..self.hovered() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: SURFACE.into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: ACTIVE, - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: Color { a: 0.5, ..SURFACE }.into(), - scroller: scrollable::Scroller { - color: HOVERED, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Slider; - - impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9.0 }, - color: ACTIVE, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } - } - - pub struct ProgressBar; - - impl progress_bar::StyleSheet for ProgressBar { - fn style(&self) -> progress_bar::Style { - progress_bar::Style { - background: SURFACE.into(), - bar: ACTIVE.into(), - border_radius: 10.0, - } - } - } - - pub struct Checkbox; - - impl checkbox::StyleSheet for Checkbox { - fn active(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: if is_checked { ACTIVE } else { SURFACE } - .into(), - checkmark_color: Color::WHITE, - border_radius: 2.0, - border_width: 1.0, - border_color: ACTIVE, - text_color: None, - } - } - - fn hovered(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Color { - a: 0.8, - ..if is_checked { ACTIVE } else { SURFACE } - } - .into(), - ..self.active(is_checked) - } - } - } - - pub struct Toggler; - - impl toggler::StyleSheet for Toggler { - fn active(&self, is_active: bool) -> toggler::Style { - toggler::Style { - background: if is_active { ACTIVE } else { SURFACE }, - background_border: None, - foreground: if is_active { Color::WHITE } else { ACTIVE }, - foreground_border: None, - } - } - - fn hovered(&self, is_active: bool) -> toggler::Style { - toggler::Style { - background: if is_active { ACTIVE } else { SURFACE }, - background_border: None, - foreground: if is_active { - Color { - a: 0.5, - ..Color::WHITE - } - } else { - Color { a: 0.5, ..ACTIVE } - }, - foreground_border: None, - } - } - } - - pub struct Rule; - - impl rule::StyleSheet for Rule { - fn style(&self) -> rule::Style { - rule::Style { - color: SURFACE, - width: 2, - radius: 1.0, - fill_mode: rule::FillMode::Padded(15), - } - } - } + fn theme(&self) -> Theme { + self.theme } } diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 8707fa3b..27d175da 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,4 +1,5 @@ -use iced::{Container, Element, Length, Sandbox, Settings, Svg}; +use iced::widget::{container, svg}; +use iced::{Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Tiger::run(Settings::default()) @@ -19,15 +20,15 @@ impl Sandbox for Tiger { fn update(&mut self, _message: ()) {} - fn view(&mut self) -> Element<()> { - let svg = Svg::from_path(format!( + fn view(&self) -> Element<()> { + let svg = svg(svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") - )) + ))) .width(Length::Fill) .height(Length::Fill); - Container::new(svg) + container(svg) .width(Length::Fill) .height(Length::Fill) .padding(20) diff --git a/examples/system_information/Cargo.toml b/examples/system_information/Cargo.toml new file mode 100644 index 00000000..7d1e4b94 --- /dev/null +++ b/examples/system_information/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "system_information" +version = "0.1.0" +authors = ["Richard <richardsoncusto@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["system"] } +bytesize = { version = "1.1.0" } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs new file mode 100644 index 00000000..af67742f --- /dev/null +++ b/examples/system_information/src/main.rs @@ -0,0 +1,149 @@ +use iced::widget::{button, column, container, text}; +use iced::{ + executor, system, Application, Command, Element, Length, Settings, Theme, +}; + +use bytesize::ByteSize; + +pub fn main() -> iced::Result { + Example::run(Settings::default()) +} + +enum Example { + Loading, + Loaded { information: system::Information }, +} + +#[derive(Clone, Debug)] +enum Message { + InformationReceived(system::Information), + Refresh, +} + +impl Application for Example { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self::Loading, + system::fetch_information(Message::InformationReceived), + ) + } + + fn title(&self) -> String { + String::from("System Information - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Refresh => { + *self = Self::Loading; + + return system::fetch_information(Message::InformationReceived); + } + Message::InformationReceived(information) => { + *self = Self::Loaded { information }; + } + } + + Command::none() + } + + fn view(&self) -> Element<Message> { + let content: Element<_> = match self { + Example::Loading => text("Loading...").size(40).into(), + Example::Loaded { information } => { + let system_name = text(format!( + "System name: {}", + information + .system_name + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let system_kernel = text(format!( + "System kernel: {}", + information + .system_kernel + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let system_version = text(format!( + "System version: {}", + information + .system_version + .as_ref() + .unwrap_or(&"unknown".to_string()) + )); + + let cpu_brand = + text(format!("Processor brand: {}", information.cpu_brand)); + + let cpu_cores = text(format!( + "Processor cores: {}", + information + .cpu_cores + .map_or("unknown".to_string(), |cores| cores + .to_string()) + )); + + let memory_readable = + ByteSize::kb(information.memory_total).to_string(); + + let memory_total = text(format!( + "Memory (total): {} kb ({})", + information.memory_total, memory_readable + )); + + let memory_text = if let Some(memory_used) = + information.memory_used + { + let memory_readable = ByteSize::kb(memory_used).to_string(); + + format!("{} kb ({})", memory_used, memory_readable) + } else { + String::from("None") + }; + + let memory_used = + text(format!("Memory (used): {}", memory_text)); + + let graphics_adapter = text(format!( + "Graphics adapter: {}", + information.graphics_adapter + )); + + let graphics_backend = text(format!( + "Graphics backend: {}", + information.graphics_backend + )); + + column![ + system_name.size(30), + system_kernel.size(30), + system_version.size(30), + cpu_brand.size(30), + cpu_cores.size(30), + memory_total.size(30), + memory_used.size(30), + graphics_adapter.size(30), + graphics_backend.size(30), + button("Refresh").on_press(Message::Refresh) + ] + .spacing(10) + .into() + } + }; + + container(content) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill) + .into() + } +} diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 5781ddef..2326ffc6 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["async-std", "debug"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +lazy_static = "1.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1.0" @@ -20,6 +21,6 @@ wasm-timer = "0.2" [package.metadata.deb] assets = [ - ["target/release/todos", "usr/bin/iced-todos", "755"], + ["target/release-opt/todos", "usr/bin/iced-todos", "755"], ["iced-todos.desktop", "usr/share/applications/", "644"], ] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 0b889407..bddc0e71 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,15 +1,31 @@ use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; -use iced::scrollable::{self, Scrollable}; -use iced::text_input::{self, TextInput}; -use iced::{ - Application, Checkbox, Column, Command, Container, Element, Font, Length, - Row, Settings, Text, +use iced::event::{self, Event}; +use iced::keyboard; +use iced::subscription; +use iced::theme::{self, Theme}; +use iced::widget::{ + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, }; +use iced::window; +use iced::{Application, Element}; +use iced::{Color, Command, Font, Length, Settings, Subscription}; + +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +lazy_static! { + static ref INPUT_ID: text_input::Id = text_input::Id::unique(); +} + pub fn main() -> iced::Result { - Todos::run(Settings::default()) + Todos::run(Settings { + window: window::Settings { + size: (500, 800), + ..window::Settings::default() + }, + ..Settings::default() + }) } #[derive(Debug)] @@ -20,12 +36,9 @@ enum Todos { #[derive(Debug, Default)] struct State { - scroll: scrollable::State, - input: text_input::State, input_value: String, filter: Filter, tasks: Vec<Task>, - controls: Controls, dirty: bool, saving: bool, } @@ -38,11 +51,13 @@ enum Message { CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, } impl Application for Todos { - type Executor = iced::executor::Default; type Message = Message; + type Theme = Theme; + type Executor = iced::executor::Default; type Flags = (); fn new(_flags: ()) -> (Todos, Command<Message>) { @@ -79,14 +94,16 @@ impl Application for Todos { _ => {} } - Command::none() + text_input::focus(INPUT_ID.clone()) } Todos::Loaded(state) => { let mut saved = false; - match message { + let command = match message { Message::InputChanged(value) => { state.input_value = value; + + Command::none() } Message::CreateTask => { if !state.input_value.is_empty() { @@ -95,30 +112,56 @@ impl Application for Todos { .push(Task::new(state.input_value.clone())); state.input_value.clear(); } + + Command::none() } Message::FilterChanged(filter) => { state.filter = filter; + + Command::none() } Message::TaskMessage(i, TaskMessage::Delete) => { state.tasks.remove(i); + + Command::none() } Message::TaskMessage(i, task_message) => { if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + task.update(task_message); + + if should_focus { + text_input::focus(Task::text_input_id(i)) + } else { + Command::none() + } + } else { + Command::none() } } Message::Saved(_) => { state.saving = false; saved = true; + + Command::none() } - _ => {} - } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + _ => Command::none(), + }; if !saved { state.dirty = true; } - if state.dirty && !state.saving { + let save = if state.dirty && !state.saving { state.dirty = false; state.saving = true; @@ -133,54 +176,57 @@ impl Application for Todos { ) } else { Command::none() - } + }; + + Command::batch(vec![command, save]) } } } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { match self { Todos::Loading => loading_message(), Todos::Loaded(State { - scroll, - input, input_value, filter, tasks, - controls, .. }) => { - let title = Text::new("todos") + let title = text("todos") .width(Length::Fill) .size(100) - .color([0.5, 0.5, 0.5]) + .style(Color::from([0.5, 0.5, 0.5])) .horizontal_alignment(alignment::Horizontal::Center); - let input = TextInput::new( - input, + let input = text_input( "What needs to be done?", input_value, Message::InputChanged, ) + .id(INPUT_ID.clone()) .padding(15) .size(30) .on_submit(Message::CreateTask); - let controls = controls.view(&tasks, *filter); + let controls = view_controls(tasks, *filter); let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); let tasks: Element<_> = if filtered_tasks.count() > 0 { - tasks - .iter_mut() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(Column::new().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) - .into() + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect(), + ) + .spacing(10) + .into() } else { empty_message(match filter { Filter::All => "You have not created a task yet...", @@ -191,23 +237,36 @@ impl Application for Todos { }) }; - let content = Column::new() - .max_width(800) + let content = column![title, input, controls, tasks] .spacing(20) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - Scrollable::new(scroll) - .padding(40) - .push( - Container::new(content).width(Length::Fill).center_x(), - ) - .into() + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(), + ) + .into() } } } + + fn subscription(&self) -> Subscription<Message> { + subscription::events_with(|event, status| match (event, status) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }), + event::Status::Ignored, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + _ => None, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -221,20 +280,13 @@ struct Task { #[derive(Debug, Clone)] pub enum TaskState { - Idle { - edit_button: button::State, - }, - Editing { - text_input: text_input::State, - delete_button: button::State, - }, + Idle, + Editing, } impl Default for TaskState { fn default() -> Self { - TaskState::Idle { - edit_button: button::State::new(), - } + Self::Idle } } @@ -248,13 +300,15 @@ pub enum TaskMessage { } impl Task { + fn text_input_id(i: usize) -> text_input::Id { + text_input::Id::new(format!("task-{}", i)) + } + fn new(description: String) -> Self { Task { description, completed: false, - state: TaskState::Idle { - edit_button: button::State::new(), - }, + state: TaskState::Idle, } } @@ -264,150 +318,100 @@ impl Task { self.completed = completed; } TaskMessage::Edit => { - let mut text_input = text_input::State::focused(); - text_input.select_all(); - - self.state = TaskState::Editing { - text_input, - delete_button: button::State::new(), - }; + self.state = TaskState::Editing; } TaskMessage::DescriptionEdited(new_description) => { self.description = new_description; } TaskMessage::FinishEdition => { if !self.description.is_empty() { - self.state = TaskState::Idle { - edit_button: button::State::new(), - } + self.state = TaskState::Idle; } } TaskMessage::Delete => {} } } - fn view(&mut self) -> Element<TaskMessage> { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, + fn view(&self, i: usize) -> Element<TaskMessage> { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox( &self.description, + self.completed, TaskMessage::Completed, ) .width(Length::Fill); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(checkbox) - .push( - Button::new(edit_button, edit_icon()) - .on_press(TaskMessage::Edit) - .padding(10) - .style(style::Button::Icon), - ) - .into() + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(theme::Button::Text), + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } - TaskState::Editing { - text_input, - delete_button, - } => { - let text_input = TextInput::new( - text_input, + TaskState::Editing => { + let text_input = text_input( "Describe your task...", &self.description, TaskMessage::DescriptionEdited, ) + .id(Self::text_input_id(i)) .on_submit(TaskMessage::FinishEdition) .padding(10); - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push(text_input) - .push( - Button::new( - delete_button, - Row::new() - .spacing(10) - .push(delete_icon()) - .push(Text::new("Delete")), - ) + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) .on_press(TaskMessage::Delete) .padding(10) - .style(style::Button::Destructive), - ) - .into() + .style(theme::Button::Destructive) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } } } } -#[derive(Debug, Default, Clone)] -pub struct Controls { - all_button: button::State, - active_button: button::State, - completed_button: button::State, -} +fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row<Message> { - let Controls { - all_button, - active_button, - completed_button, - } = self; - - let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - - let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16); - let button = - Button::new(state, label).style(if filter == current_filter { - style::Button::FilterSelected - } else { - style::Button::FilterActive - }); + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); - button.on_press(Message::FilterChanged(filter)).padding(8) - }; - - Row::new() - .spacing(20) - .align_items(Alignment::Center) - .push( - Text::new(format!( - "{} {} left", - tasks_left, - if tasks_left == 1 { "task" } else { "tasks" } - )) - .width(Length::Fill) - .size(16), - ) - .push( - Row::new() - .width(Length::Shrink) - .spacing(10) - .push(filter_button( - all_button, - "All", - Filter::All, - current_filter, - )) - .push(filter_button( - active_button, - "Active", - Filter::Active, - current_filter, - )) - .push(filter_button( - completed_button, - "Completed", - Filter::Completed, - current_filter, - )), - ) - } + let button = button(label).style(if filter == current_filter { + theme::Button::Primary + } else { + theme::Button::Text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_items(Alignment::Center) + .into() } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -434,8 +438,8 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - Container::new( - Text::new("Loading...") + container( + text("Loading...") .horizontal_alignment(alignment::Horizontal::Center) .size(50), ) @@ -445,13 +449,13 @@ fn loading_message<'a>() -> Element<'a, Message> { .into() } -fn empty_message<'a>(message: &str) -> Element<'a, Message> { - Container::new( - Text::new(message) +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) .width(Length::Fill) .size(25) .horizontal_alignment(alignment::Horizontal::Center) - .color([0.7, 0.7, 0.7]), + .style(Color::from([0.7, 0.7, 0.7])), ) .width(Length::Fill) .height(Length::Units(200)) @@ -462,22 +466,22 @@ fn empty_message<'a>(message: &str) -> Element<'a, Message> { // Fonts const ICONS: Font = Font::External { name: "Icons", - bytes: include_bytes!("../fonts/icons.ttf"), + bytes: include_bytes!("../../todos/fonts/icons.ttf"), }; -fn icon(unicode: char) -> Text { - Text::new(unicode.to_string()) +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) .font(ICONS) .width(Length::Units(20)) .horizontal_alignment(alignment::Horizontal::Center) .size(20) } -fn edit_icon() -> Text { +fn edit_icon() -> Text<'static> { icon('\u{F303}') } -fn delete_icon() -> Text { +fn delete_icon() -> Text<'static> { icon('\u{F1F8}') } @@ -491,15 +495,15 @@ struct SavedState { #[derive(Debug, Clone)] enum LoadError { - FileError, - FormatError, + File, + Format, } #[derive(Debug, Clone)] enum SaveError { - FileError, - WriteError, - FormatError, + File, + Write, + Format, } #[cfg(not(target_arch = "wasm32"))] @@ -510,7 +514,7 @@ impl SavedState { { project_dirs.data_dir().into() } else { - std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + std::env::current_dir().unwrap_or_default() }; path.push("todos.json"); @@ -525,37 +529,37 @@ impl SavedState { let mut file = async_std::fs::File::open(Self::path()) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; file.read_to_string(&mut contents) .await - .map_err(|_| LoadError::FileError)?; + .map_err(|_| LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + serde_json::from_str(&contents).map_err(|_| LoadError::Format) } async fn save(self) -> Result<(), SaveError> { use async_std::prelude::*; let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; + .map_err(|_| SaveError::Format)?; let path = Self::path(); if let Some(dir) = path.parent() { async_std::fs::create_dir_all(dir) .await - .map_err(|_| SaveError::FileError)?; + .map_err(|_| SaveError::File)?; } { let mut file = async_std::fs::File::create(path) .await - .map_err(|_| SaveError::FileError)?; + .map_err(|_| SaveError::File)?; file.write_all(json.as_bytes()) .await - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; } // This is a simple way to save at most once every couple seconds @@ -574,82 +578,28 @@ impl SavedState { } async fn load() -> Result<SavedState, LoadError> { - let storage = Self::storage().ok_or(LoadError::FileError)?; + let storage = Self::storage().ok_or(LoadError::File)?; let contents = storage .get_item("state") - .map_err(|_| LoadError::FileError)? - .ok_or(LoadError::FileError)?; + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + serde_json::from_str(&contents).map_err(|_| LoadError::Format) } async fn save(self) -> Result<(), SaveError> { - let storage = Self::storage().ok_or(SaveError::FileError)?; + let storage = Self::storage().ok_or(SaveError::File)?; let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; + .map_err(|_| SaveError::Format)?; storage .set_item("state", &json) - .map_err(|_| SaveError::WriteError)?; + .map_err(|_| SaveError::Write)?; let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; Ok(()) } } - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - FilterActive, - FilterSelected, - Icon, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - match self { - Button::FilterActive => button::Style::default(), - Button::FilterSelected => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.2, 0.2, 0.7, - ))), - border_radius: 10.0, - text_color: Color::WHITE, - ..button::Style::default() - }, - Button::Icon => button::Style { - text_color: Color::from_rgb(0.5, 0.5, 0.5), - ..button::Style::default() - }, - Button::Destructive => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.8, 0.2, 0.2, - ))), - border_radius: 5.0, - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 1.0), - ..button::Style::default() - }, - } - } - - fn hovered(&self) -> button::Style { - let active = self.active(); - - button::Style { - text_color: match self { - Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), - Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7), - _ => active.text_color, - }, - shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), - ..active - } - } - } -} diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index cfeaf6a6..35b862a8 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -1,138 +1,75 @@ -use iced::tooltip::{self, Tooltip}; -use iced::{ - alignment, button, Alignment, Button, Column, Container, Element, Length, - Row, Sandbox, Settings, Text, -}; +use iced::theme; +use iced::widget::tooltip::Position; +use iced::widget::{button, container, tooltip}; +use iced::{Element, Length, Sandbox, Settings}; -pub fn main() { - Example::run(Settings::default()).unwrap() +pub fn main() -> iced::Result { + Example::run(Settings::default()) } -#[derive(Default)] struct Example { - top: button::State, - bottom: button::State, - right: button::State, - left: button::State, - follow_cursor: button::State, + position: Position, } -#[derive(Debug, Clone, Copy)] -struct Message; +#[derive(Debug, Clone)] +enum Message { + ChangePosition, +} impl Sandbox for Example { type Message = Message; fn new() -> Self { - Self::default() + Self { + position: Position::Bottom, + } } fn title(&self) -> String { String::from("Tooltip - Iced") } - fn update(&mut self, _message: Message) {} - - fn view(&mut self) -> Element<Message> { - let top = - tooltip("Tooltip at top", &mut self.top, tooltip::Position::Top); - - let bottom = tooltip( - "Tooltip at bottom", - &mut self.bottom, - tooltip::Position::Bottom, - ); - - let left = - tooltip("Tooltip at left", &mut self.left, tooltip::Position::Left); - - let right = tooltip( - "Tooltip at right", - &mut self.right, - tooltip::Position::Right, - ); - - let fixed_tooltips = Row::with_children(vec![ - top.into(), - bottom.into(), - left.into(), - right.into(), - ]) - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::Center) - .spacing(50); - - let follow_cursor = tooltip( - "Tooltip follows cursor", - &mut self.follow_cursor, - tooltip::Position::FollowCursor, - ); + fn update(&mut self, message: Message) { + match message { + Message::ChangePosition => { + let position = match &self.position { + Position::FollowCursor => Position::Top, + Position::Top => Position::Bottom, + Position::Bottom => Position::Left, + Position::Left => Position::Right, + Position::Right => Position::FollowCursor, + }; + + self.position = position + } + } + } - let content = Column::with_children(vec![ - Container::new(fixed_tooltips) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into(), - follow_cursor.into(), - ]) - .width(Length::Fill) - .height(Length::Fill) - .spacing(50); + fn view(&self) -> Element<Message> { + let tooltip = tooltip( + button("Press to change position") + .on_press(Message::ChangePosition), + position_to_text(self.position), + self.position, + ) + .gap(10) + .style(theme::Container::Box); - Container::new(content) + container(tooltip) .width(Length::Fill) .height(Length::Fill) .center_x() .center_y() - .padding(50) .into() } } -fn tooltip<'a>( - label: &str, - button_state: &'a mut button::State, - position: tooltip::Position, -) -> Element<'a, Message> { - Tooltip::new( - Button::new( - button_state, - Text::new(label) - .size(40) - .width(Length::Fill) - .height(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), - ) - .on_press(Message) - .width(Length::Fill) - .height(Length::Fill), - "Tooltip", - position, - ) - .gap(5) - .padding(10) - .style(style::Tooltip) - .into() -} - -mod style { - use iced::container; - use iced::Color; - - pub struct Tooltip; - - impl container::StyleSheet for Tooltip { - fn style(&self) -> container::Style { - container::Style { - text_color: Some(Color::from_rgb8(0xEE, 0xEE, 0xEE)), - background: Some(Color::from_rgb(0.11, 0.42, 0.87).into()), - border_radius: 12.0, - ..container::Style::default() - } - } +fn position_to_text<'a>(position: Position) -> &'a str { + match position { + Position::FollowCursor => "Follow Cursor", + Position::Top => "Top", + Position::Bottom => "Bottom", + Position::Left => "Left", + Position::Right => "Right", } } diff --git a/examples/tour/README.md b/examples/tour/README.md index e7cd2d5c..731e7e66 100644 --- a/examples/tour/README.md +++ b/examples/tour/README.md @@ -23,6 +23,11 @@ You can run the native version with `cargo run`: cargo run --package tour ``` -The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)! +The web version can be run with [`trunk`]: -[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage +``` +cd examples/tour +trunk serve +``` + +[`trunk`]: https://trunkrs.dev/ diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index e199c88c..378508e1 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,8 +1,11 @@ -use iced::{ - alignment, button, scrollable, slider, text_input, Button, Checkbox, Color, - Column, Container, ContentFit, Element, Image, Length, Radio, Row, Sandbox, - Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, +use iced::alignment; +use iced::theme; +use iced::widget::{ + checkbox, column, container, horizontal_space, image, radio, row, + scrollable, slider, text, text_input, toggler, vertical_space, }; +use iced::widget::{Button, Column, Container, Slider}; +use iced::{Color, Element, Length, Renderer, Sandbox, Settings}; pub fn main() -> iced::Result { env_logger::init(); @@ -12,9 +15,6 @@ pub fn main() -> iced::Result { pub struct Tour { steps: Steps, - scroll: scrollable::State, - back_button: button::State, - next_button: button::State, debug: bool, } @@ -24,9 +24,6 @@ impl Sandbox for Tour { fn new() -> Tour { Tour { steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), debug: false, } } @@ -49,56 +46,49 @@ impl Sandbox for Tour { } } - fn view(&mut self) -> Element<Message> { - let Tour { - steps, - scroll, - back_button, - next_button, - .. - } = self; + fn view(&self) -> Element<Message> { + let Tour { steps, .. } = self; - let mut controls = Row::new(); + let mut controls = row![]; if steps.has_previous() { controls = controls.push( - button(back_button, "Back") + button("Back") .on_press(Message::BackPressed) - .style(style::Button::Secondary), + .style(theme::Button::Secondary), ); } - controls = controls.push(Space::with_width(Length::Fill)); + controls = controls.push(horizontal_space(Length::Fill)); if steps.can_continue() { controls = controls.push( - button(next_button, "Next") + button("Next") .on_press(Message::NextPressed) - .style(style::Button::Primary), + .style(theme::Button::Primary), ); } - let content: Element<_> = Column::new() - .max_width(540) - .spacing(20) - .padding(20) - .push(steps.view(self.debug).map(Message::StepMessage)) - .push(controls) - .into(); - - let content = if self.debug { - content.explain(Color::BLACK) - } else { - content - }; - - let scrollable = Scrollable::new(scroll) - .push(Container::new(content).width(Length::Fill).center_x()); + let content: Element<_> = column![ + steps.view(self.debug).map(Message::StepMessage), + controls, + ] + .max_width(540) + .spacing(20) + .padding(20) + .into(); + + let scrollable = scrollable( + container(if self.debug { + content.explain(Color::BLACK) + } else { + content + }) + .width(Length::Fill) + .center_x(), + ); - Container::new(scrollable) - .height(Length::Fill) - .center_y() - .into() + container(scrollable).height(Length::Fill).center_y().into() } } @@ -119,35 +109,24 @@ impl Steps { Steps { steps: vec![ Step::Welcome, - Step::Slider { - state: slider::State::new(), - value: 50, - }, + Step::Slider { value: 50 }, Step::RowsAndColumns { layout: Layout::Row, - spacing_slider: slider::State::new(), spacing: 20, }, Step::Text { - size_slider: slider::State::new(), size: 30, - color_sliders: [slider::State::new(); 3], color: Color::BLACK, }, Step::Radio { selection: None }, Step::Toggler { can_continue: false, }, - Step::Image { - height: 200, - current_fit: ContentFit::Contain, - slider: slider::State::new(), - }, + Step::Image { width: 300 }, Step::Scrollable, Step::TextInput { value: String::new(), is_secure: false, - state: text_input::State::new(), }, Step::Debugger, Step::End, @@ -160,7 +139,7 @@ impl Steps { self.steps[self.current].update(msg, debug); } - fn view(&mut self, debug: bool) -> Element<StepMessage> { + fn view(&self, debug: bool) -> Element<StepMessage> { self.steps[self.current].view(debug) } @@ -192,38 +171,14 @@ impl Steps { enum Step { Welcome, - Slider { - state: slider::State, - value: u8, - }, - RowsAndColumns { - layout: Layout, - spacing_slider: slider::State, - spacing: u16, - }, - Text { - size_slider: slider::State, - size: u16, - color_sliders: [slider::State; 3], - color: Color, - }, - Radio { - selection: Option<Language>, - }, - Toggler { - can_continue: bool, - }, - Image { - height: u16, - slider: slider::State, - current_fit: ContentFit, - }, + Slider { value: u8 }, + RowsAndColumns { layout: Layout, spacing: u16 }, + Text { size: u16, color: Color }, + Radio { selection: Option<Language> }, + Toggler { can_continue: bool }, + Image { width: u16 }, Scrollable, - TextInput { - value: String, - is_secure: bool, - state: text_input::State, - }, + TextInput { value: String, is_secure: bool }, Debugger, End, } @@ -236,8 +191,7 @@ pub enum StepMessage { TextSizeChanged(u16), TextColorChanged(Color), LanguageSelected(Language), - ImageHeightChanged(u16), - ImageFitSelected(ContentFit), + ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), DebugToggled(bool), @@ -282,14 +236,9 @@ impl<'a> Step { *spacing = new_spacing; } } - StepMessage::ImageHeightChanged(new_height) => { - if let Step::Image { height, .. } = self { - *height = new_height; - } - } - StepMessage::ImageFitSelected(fit) => { - if let Step::Image { current_fit, .. } = self { - *current_fit = fit; + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width; } } StepMessage::InputChanged(new_value) => { @@ -342,34 +291,21 @@ impl<'a> Step { } } - fn view(&mut self, debug: bool) -> Element<StepMessage> { + fn view(&self, debug: bool) -> Element<StepMessage> { match self { Step::Welcome => Self::welcome(), Step::Radio { selection } => Self::radio(*selection), Step::Toggler { can_continue } => Self::toggler(*can_continue), - Step::Slider { state, value } => Self::slider(state, *value), - Step::Text { - size_slider, - size, - color_sliders, - color, - } => Self::text(size_slider, *size, color_sliders, *color), - Step::Image { - height, - slider, - current_fit, - } => Self::image(*height, slider, *current_fit), - Step::RowsAndColumns { - layout, - spacing_slider, - spacing, - } => Self::rows_and_columns(*layout, spacing_slider, *spacing), + Step::Slider { value } => Self::slider(*value), + Step::Text { size, color } => Self::text(*size, *color), + Step::Image { width } => Self::image(*width), + Step::RowsAndColumns { layout, spacing } => { + Self::rows_and_columns(*layout, *spacing) + } Step::Scrollable => Self::scrollable(), - Step::TextInput { - value, - is_secure, - state, - } => Self::text_input(value, *is_secure, state), + Step::TextInput { value, is_secure } => { + Self::text_input(value, *is_secure) + } Step::Debugger => Self::debugger(debug), Step::End => Self::end(), } @@ -377,59 +313,51 @@ impl<'a> Step { } fn container(title: &str) -> Column<'a, StepMessage> { - Column::new().spacing(20).push(Text::new(title).size(50)) + column![text(title).size(50)].spacing(20) } fn welcome() -> Column<'a, StepMessage> { Self::container("Welcome!") - .push(Text::new( + .push( "This is a simple tour meant to showcase a bunch of widgets \ that can be easily implemented on top of Iced.", - )) - .push(Text::new( + ) + .push( "Iced is a cross-platform GUI library for Rust focused on \ simplicity and type-safety. It is heavily inspired by Elm.", - )) - .push(Text::new( + ) + .push( "It was originally born as part of Coffee, an opinionated \ 2D game engine for Rust.", - )) - .push(Text::new( + ) + .push( "On native platforms, Iced provides by default a renderer \ built on top of wgpu, a graphics library supporting Vulkan, \ Metal, DX11, and DX12.", - )) - .push(Text::new( + ) + .push( "Additionally, this tour can also run on WebAssembly thanks \ to dodrio, an experimental VDOM library for Rust.", - )) - .push(Text::new( + ) + .push( "You will need to interact with the UI in order to reach the \ end!", - )) + ) } - fn slider( - state: &'a mut slider::State, - value: u8, - ) -> Column<'a, StepMessage> { + fn slider(value: u8) -> Column<'a, StepMessage> { Self::container("Slider") - .push(Text::new( + .push( "A slider allows you to smoothly select a value from a range \ of values.", - )) - .push(Text::new( + ) + .push( "The following slider lets you choose an integer from \ 0 to 100:", - )) - .push(Slider::new( - state, - 0..=100, - value, - StepMessage::SliderChanged, - )) + ) + .push(slider(0..=100, value, StepMessage::SliderChanged)) .push( - Text::new(value.to_string()) + text(value.to_string()) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) @@ -437,257 +365,198 @@ impl<'a> Step { fn rows_and_columns( layout: Layout, - spacing_slider: &'a mut slider::State, spacing: u16, ) -> Column<'a, StepMessage> { - let row_radio = Radio::new( - Layout::Row, - "Row", - Some(layout), - StepMessage::LayoutChanged, - ); + let row_radio = + radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); - let column_radio = Radio::new( - Layout::Column, + let column_radio = radio( "Column", + Layout::Column, Some(layout), StepMessage::LayoutChanged, ); let layout_section: Element<_> = match layout { - Layout::Row => Row::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - Layout::Column => Column::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), + Layout::Row => { + row![row_radio, column_radio].spacing(spacing).into() + } + Layout::Column => { + column![row_radio, column_radio].spacing(spacing).into() + } }; - let spacing_section = Column::new() - .spacing(10) - .push(Slider::new( - spacing_slider, - 0..=80, - spacing, - StepMessage::SpacingChanged, - )) - .push( - Text::new(format!("{} px", spacing)) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center), - ); + let spacing_section = column![ + slider(0..=80, spacing, StepMessage::SpacingChanged), + text(format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ] + .spacing(10); Self::container("Rows and columns") .spacing(spacing) - .push(Text::new( + .push( "Iced uses a layout model based on flexbox to position UI \ elements.", - )) - .push(Text::new( + ) + .push( "Rows and columns can be used to distribute content \ horizontally or vertically, respectively.", - )) + ) .push(layout_section) - .push(Text::new( - "You can also easily change the spacing between elements:", - )) + .push("You can also easily change the spacing between elements:") .push(spacing_section) } - fn text( - size_slider: &'a mut slider::State, - size: u16, - color_sliders: &'a mut [slider::State; 3], - color: Color, - ) -> Column<'a, StepMessage> { - let size_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("You can change its size:")) - .push(Text::new(format!("This text is {} pixels", size)).size(size)) - .push(Slider::new( - size_slider, - 10..=70, - size, - StepMessage::TextSizeChanged, - )); - - let [red, green, blue] = color_sliders; - - let color_sliders = Row::new() - .spacing(10) - .push(color_slider(red, color.r, move |r| Color { r, ..color })) - .push(color_slider(green, color.g, move |g| Color { g, ..color })) - .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + fn text(size: u16, color: Color) -> Column<'a, StepMessage> { + let size_section = column![ + "You can change its size:", + text(format!("This text is {} pixels", size)).size(size), + slider(10..=70, size, StepMessage::TextSizeChanged), + ] + .padding(20) + .spacing(20); - let color_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("And its color:")) - .push(Text::new(format!("{:?}", color)).color(color)) - .push(color_sliders); + let color_sliders = row![ + color_slider(color.r, move |r| Color { r, ..color }), + color_slider(color.g, move |g| Color { g, ..color }), + color_slider(color.b, move |b| Color { b, ..color }), + ] + .spacing(10); + + let color_section = column![ + "And its color:", + text(format!("{:?}", color)).style(color), + color_sliders, + ] + .padding(20) + .spacing(20); Self::container("Text") - .push(Text::new( + .push( "Text is probably the most essential widget for your UI. \ It will try to adapt to the dimensions of its container.", - )) + ) .push(size_section) .push(color_section) } fn radio(selection: Option<Language>) -> Column<'a, StepMessage> { - let question = Column::new() - .padding(20) + let question = column![ + text("Iced is written in...").size(24), + column( + Language::all() + .iter() + .cloned() + .map(|language| { + radio( + language, + language, + selection, + StepMessage::LanguageSelected, + ) + }) + .map(Element::from) + .collect() + ) .spacing(10) - .push(Text::new("Iced is written in...").size(24)) - .push(Language::all().iter().cloned().fold( - Column::new().padding(10).spacing(20), - |choices, language| { - choices.push(Radio::new( - language, - language, - selection, - StepMessage::LanguageSelected, - )) - }, - )); + ] + .padding(20) + .spacing(10); Self::container("Radio button") - .push(Text::new( + .push( "A radio button is normally used to represent a choice... \ Surprise test!", - )) + ) .push(question) - .push(Text::new( + .push( "Iced works very well with iterators! The list above is \ basically created by folding a column over the different \ choices, creating a radio button for each one of them!", - )) + ) } fn toggler(can_continue: bool) -> Column<'a, StepMessage> { Self::container("Toggler") - .push(Text::new( - "A toggler is mostly used to enable or disable something.", - )) + .push("A toggler is mostly used to enable or disable something.") .push( - Container::new(Toggler::new( + Container::new(toggler( + "Toggle me to continue...".to_owned(), can_continue, - String::from("Toggle me to continue..."), StepMessage::TogglerChanged, )) .padding([0, 40]), ) } - fn image( - height: u16, - slider: &'a mut slider::State, - current_fit: ContentFit, - ) -> Column<'a, StepMessage> { - const FIT_MODES: [(ContentFit, &str); 3] = [ - (ContentFit::Contain, "Contain"), - (ContentFit::Cover, "Cover"), - (ContentFit::Fill, "Fill"), - ]; - - let mode_selector = FIT_MODES.iter().fold( - Column::new().padding(10).spacing(20), - |choices, (mode, name)| { - choices.push(Radio::new( - *mode, - *name, - Some(current_fit), - StepMessage::ImageFitSelected, - )) - }, - ); - + fn image(width: u16) -> Column<'a, StepMessage> { Self::container("Image") - .push(Text::new("Pictures of things in all shapes and sizes!")) - .push(ferris(height, current_fit)) - .push(Slider::new( - slider, - 50..=500, - height, - StepMessage::ImageHeightChanged, - )) + .push("An image that tries to keep its aspect ratio.") + .push(ferris(width)) + .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push( - Text::new(format!("Height: {} px", height)) + text(format!("Width: {} px", width)) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) - .push(Text::new("Pick a content fit strategy:")) - .push(mode_selector) } fn scrollable() -> Column<'a, StepMessage> { Self::container("Scrollable") - .push(Text::new( + .push( "Iced supports scrollable content. Try it out! Find the \ button further below.", - )) + ) .push( - Text::new( - "Tip: You can use the scrollbar to scroll down faster!", - ) - .size(16), + text("Tip: You can use the scrollbar to scroll down faster!") + .size(16), ) - .push(Column::new().height(Length::Units(4096))) + .push(vertical_space(Length::Units(4096))) .push( - Text::new("You are halfway there!") + text("You are halfway there!") .width(Length::Fill) .size(30) .horizontal_alignment(alignment::Horizontal::Center), ) - .push(Column::new().height(Length::Units(4096))) - .push(ferris(200, ContentFit::Contain)) + .push(vertical_space(Length::Units(4096))) + .push(ferris(300)) .push( - Text::new("You made it!") + text("You made it!") .width(Length::Fill) .size(50) .horizontal_alignment(alignment::Horizontal::Center), ) } - fn text_input( - value: &str, - is_secure: bool, - state: &'a mut text_input::State, - ) -> Column<'a, StepMessage> { - let text_input = TextInput::new( - state, + fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { + let text_input = text_input( "Type something to continue...", value, StepMessage::InputChanged, ) .padding(10) .size(30); + Self::container("Text input") - .push(Text::new( - "Use a text input to ask for different kinds of information.", - )) + .push("Use a text input to ask for different kinds of information.") .push(if is_secure { text_input.password() } else { text_input }) - .push(Checkbox::new( - is_secure, + .push(checkbox( "Enable password mode", + is_secure, StepMessage::ToggleSecureInput, )) - .push(Text::new( + .push( "A text input produces a message every time it changes. It is \ very easy to keep track of its contents:", - )) + ) .push( - Text::new(if value.is_empty() { + text(if value.is_empty() { "You have not typed anything yet..." } else { value @@ -699,79 +568,62 @@ impl<'a> Step { fn debugger(debug: bool) -> Column<'a, StepMessage> { Self::container("Debugger") - .push(Text::new( + .push( "You can ask Iced to visually explain the layouting of the \ different elements comprising your UI!", - )) - .push(Text::new( + ) + .push( "Give it a shot! Check the following checkbox to be able to \ see element boundaries.", - )) + ) .push(if cfg!(target_arch = "wasm32") { Element::new( - Text::new("Not available on web yet!") - .color([0.7, 0.7, 0.7]) + text("Not available on web yet!") + .style(Color::from([0.7, 0.7, 0.7])) .horizontal_alignment(alignment::Horizontal::Center), ) } else { - Element::new(Checkbox::new( - debug, - "Explain layout", - StepMessage::DebugToggled, - )) + checkbox("Explain layout", debug, StepMessage::DebugToggled) + .into() }) - .push(Text::new("Feel free to go back and take a look.")) + .push("Feel free to go back and take a look.") } fn end() -> Column<'a, StepMessage> { Self::container("You reached the end!") - .push(Text::new( - "This tour will be updated as more features are added.", - )) - .push(Text::new("Make sure to keep an eye on it!")) + .push("This tour will be updated as more features are added.") + .push("Make sure to keep an eye on it!") } } -fn ferris<'a>( - height: u16, - content_fit: ContentFit, -) -> Container<'a, StepMessage> { - Container::new( +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { + container( // This should go away once we unify resource loading on native // platforms if cfg!(target_arch = "wasm32") { - Image::new("tour/images/ferris.png") + image("tour/images/ferris.png") } else { - Image::new(format!( - "{}/images/ferris.png", - env!("CARGO_MANIFEST_DIR"), - )) + image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } - .height(Length::Units(height)) - .content_fit(content_fit), + .width(Length::Units(width)), ) .width(Length::Fill) .center_x() } -fn button<'a, Message: Clone>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - Button::new( - state, - Text::new(label).horizontal_alignment(alignment::Horizontal::Center), +fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + iced::widget::button( + text(label).horizontal_alignment(alignment::Horizontal::Center), ) .padding(12) - .min_width(100) + .width(Length::Units(100)) } -fn color_slider( - state: &mut slider::State, +fn color_slider<'a>( component: f32, - update: impl Fn(f32) -> Color + 'static, -) -> Slider<f64, StepMessage> { - Slider::new(state, 0.0..=1.0, f64::from(component), move |c| { + update: impl Fn(f32) -> Color + 'a, +) -> Slider<'a, f64, StepMessage, Renderer> { + slider(0.0..=1.0, f64::from(component), move |c| { StepMessage::TextColorChanged(update(c as f32)) }) .step(0.01) @@ -818,36 +670,3 @@ pub enum Layout { Row, Column, } - -mod style { - use iced::button; - use iced::{Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - })), - border_radius: 12.0, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } -} diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index ee2d249a..3257b519 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,6 +1,7 @@ +use iced::executor; +use iced::widget::{container, text}; use iced::{ - executor, Application, Command, Container, Element, Length, Settings, - Subscription, Text, + Application, Command, Element, Length, Settings, Subscription, Theme, }; use iced_native::{ event::{MacOS, PlatformSpecific}, @@ -22,8 +23,9 @@ enum Message { } impl Application for App { - type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (App, Command<Message>) { @@ -53,13 +55,13 @@ impl Application for App { iced_native::subscription::events().map(Message::EventOccurred) } - fn view(&mut self) -> Element<Message> { + fn view(&self) -> Element<Message> { let content = match &self.url { - Some(url) => Text::new(format!("{}", url)), - None => Text::new("No URL received yet!"), + Some(url) => text(url), + None => text("No URL received yet!"), }; - Container::new(content.size(48)) + container(content.size(48)) .width(Length::Fill) .height(Length::Fill) .center_x() diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index db131dd7..3981f699 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "websocket" -version = "0.1.0" +version = "1.0.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" publish = false @@ -9,6 +9,7 @@ publish = false iced = { path = "../..", features = ["tokio", "debug"] } iced_native = { path = "../../native" } iced_futures = { path = "../../futures" } +lazy_static = "1.4" [dependencies.async-tungstenite] version = "0.16" diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index 13596ddd..ae65e064 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -8,6 +8,7 @@ use futures::sink::SinkExt; use futures::stream::StreamExt; use async_tungstenite::tungstenite; +use std::fmt; pub fn connect() -> Subscription<Event> { struct Connect; @@ -32,7 +33,7 @@ pub fn connect() -> Subscription<Event> { ) } Err(_) => { - let _ = tokio::time::sleep( + tokio::time::sleep( tokio::time::Duration::from_secs(1), ) .await; @@ -63,7 +64,7 @@ pub fn connect() -> Subscription<Event> { } message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(String::from(message))).await; + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; if result.is_ok() { (None, State::Connected(websocket, input)) @@ -79,6 +80,7 @@ pub fn connect() -> Subscription<Event> { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum State { Disconnected, Connected( @@ -101,8 +103,7 @@ pub struct Connection(mpsc::Sender<Message>); impl Connection { pub fn send(&mut self, message: Message) { - let _ = self - .0 + self.0 .try_send(message) .expect("Send message to echo server"); } @@ -133,14 +134,14 @@ impl Message { } } -impl From<Message> for String { - fn from(message: Message) -> Self { - match message { - Message::Connected => String::from("Connected successfully!"), +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Message::Connected => write!(f, "Connected successfully!"), Message::Disconnected => { - String::from("Connection lost... Retrying...") + write!(f, "Connection lost... Retrying...") } - Message::User(message) => message, + Message::User(message) => write!(f, "{}", message), } } } diff --git a/examples/websocket/src/echo/server.rs b/examples/websocket/src/echo/server.rs index 7702d417..fef89a12 100644 --- a/examples/websocket/src/echo/server.rs +++ b/examples/websocket/src/echo/server.rs @@ -27,9 +27,9 @@ use warp::Filter; // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. pub async fn run() { - let routes = warp::path::end().and(warp::ws()).map(|ws: warp::ws::Ws| { - ws.on_upgrade(move |socket| user_connected(socket)) - }); + let routes = warp::path::end() + .and(warp::ws()) + .map(|ws: warp::ws::Ws| ws.on_upgrade(user_connected)); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } @@ -40,7 +40,7 @@ async fn user_connected(ws: WebSocket) { tokio::task::spawn(async move { while let Some(message) = rx.next().await { - let _ = user_ws_tx.send(message).await.unwrap_or_else(|e| { + user_ws_tx.send(message).await.unwrap_or_else(|e| { eprintln!("websocket send error: {}", e); }); } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index c03a9f3a..3902e04c 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,13 +1,12 @@ mod echo; use iced::alignment::{self, Alignment}; -use iced::button::{self, Button}; use iced::executor; -use iced::scrollable::{self, Scrollable}; -use iced::text_input::{self, TextInput}; +use iced::widget::{ + button, column, container, row, scrollable, text, text_input, Column, +}; use iced::{ - Application, Color, Column, Command, Container, Element, Length, Row, - Settings, Subscription, Text, + Application, Color, Command, Element, Length, Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -17,10 +16,7 @@ pub fn main() -> iced::Result { #[derive(Default)] struct WebSocket { messages: Vec<echo::Message>, - message_log: scrollable::State, new_message: String, - new_message_state: text_input::State, - new_message_button: button::State, state: State, } @@ -34,6 +30,7 @@ enum Message { impl Application for WebSocket { type Message = Message; + type Theme = Theme; type Flags = (); type Executor = executor::Default; @@ -52,46 +49,53 @@ impl Application for WebSocket { match message { Message::NewMessageChanged(new_message) => { self.new_message = new_message; + + Command::none() } Message::Send(message) => match &mut self.state { State::Connected(connection) => { self.new_message.clear(); connection.send(message); + + Command::none() } - State::Disconnected => {} + State::Disconnected => Command::none(), }, Message::Echo(event) => match event { echo::Event::Connected(connection) => { self.state = State::Connected(connection); self.messages.push(echo::Message::connected()); + + Command::none() } echo::Event::Disconnected => { self.state = State::Disconnected; self.messages.push(echo::Message::disconnected()); + + Command::none() } echo::Event::MessageReceived(message) => { self.messages.push(message); - self.message_log.snap_to(1.0); + + scrollable::snap_to(MESSAGE_LOG.clone(), 1.0) } }, - Message::Server => {} + Message::Server => Command::none(), } - - Command::none() } fn subscription(&self) -> Subscription<Message> { echo::connect().map(Message::Echo) } - fn view(&mut self) -> Element<Message> { - let message_log = if self.messages.is_empty() { - Container::new( - Text::new("Your messages will appear here...") - .color(Color::from_rgb8(0x88, 0x88, 0x88)), + fn view(&self) -> Element<Message> { + let message_log: Element<_> = if self.messages.is_empty() { + container( + text("Your messages will appear here...") + .style(Color::from_rgb8(0x88, 0x88, 0x88)), ) .width(Length::Fill) .height(Length::Fill) @@ -99,31 +103,33 @@ impl Application for WebSocket { .center_y() .into() } else { - self.messages - .iter() - .cloned() - .fold( - Scrollable::new(&mut self.message_log), - |scrollable, message| scrollable.push(Text::new(message)), + scrollable( + Column::with_children( + self.messages + .iter() + .cloned() + .map(text) + .map(Element::from) + .collect(), ) .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .into() + .spacing(10), + ) + .id(MESSAGE_LOG.clone()) + .height(Length::Fill) + .into() }; let new_message_input = { - let mut input = TextInput::new( - &mut self.new_message_state, + let mut input = text_input( "Type a message...", &self.new_message, Message::NewMessageChanged, ) .padding(10); - let mut button = Button::new( - &mut self.new_message_button, - Text::new("Send") + let mut button = button( + text("Send") .height(Length::Fill) .vertical_alignment(alignment::Vertical::Center), ) @@ -136,12 +142,10 @@ impl Application for WebSocket { } } - Row::with_children(vec![input.into(), button.into()]) - .spacing(10) - .align_items(Alignment::Fill) + row![input, button].spacing(10).align_items(Alignment::Fill) }; - Column::with_children(vec![message_log, new_message_input.into()]) + column![message_log, new_message_input] .width(Length::Fill) .height(Length::Fill) .padding(20) @@ -160,3 +164,7 @@ impl Default for State { Self::Disconnected } } + +lazy_static::lazy_static! { + static ref MESSAGE_LOG: scrollable::Id = scrollable::Id::unique(); +} diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 78e673e0..61ee00a5 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_futures" -version = "0.3.0" +version = "0.4.1" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "Commands, subscriptions, and runtimes for Iced" diff --git a/futures/src/command.rs b/futures/src/command.rs index d8adfe49..05c3a1d0 100644 --- a/futures/src/command.rs +++ b/futures/src/command.rs @@ -17,7 +17,7 @@ impl<T> Command<T> { Self(Internal::None) } - /// Creates a [`Command`] that performs a single [`Action`]. + /// Creates a [`Command`] that performs a single action. pub const fn single(action: T) -> Self { Self(Internal::Single(action)) } diff --git a/futures/src/lib.rs b/futures/src/lib.rs index b0b2f6ce..c0982db7 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -4,13 +4,19 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![forbid(unsafe_code)] -#![forbid(rust_2018_idioms)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] - pub use futures; mod command; diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 2034ed6c..24f9f241 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -9,6 +9,8 @@ use std::marker::PhantomData; /// /// If you have an [`Executor`], a [`Runtime`] can be leveraged to run any /// [`Command`] or [`Subscription`] and get notified of the results! +/// +/// [`Command`]: crate::Command #[derive(Debug)] pub struct Runtime<Hasher, Event, Executor, Sender, Message> { executor: Executor, @@ -51,10 +53,12 @@ where self.executor.enter(f) } - /// Spawns a [`Command`] in the [`Runtime`]. + /// Spawns a [`Future`] in the [`Runtime`]. /// /// The resulting `Message` will be forwarded to the `Sender` of the /// [`Runtime`]. + /// + /// [`Future`]: BoxFuture pub fn spawn(&mut self, future: BoxFuture<Message>) { use futures::{FutureExt, SinkExt}; @@ -62,8 +66,6 @@ where let future = future.then(|message| async move { let _ = sender.send(message).await; - - () }); self.executor.spawn(future); diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 6f261827..0479c63c 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -125,9 +125,9 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.3/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.3/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.3/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.4/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.4/examples/stopwatch pub trait Recipe<Hasher: std::hash::Hasher, Event> { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. @@ -183,11 +183,7 @@ where let mapper = self.mapper; - Box::pin( - self.recipe - .stream(input) - .map(move |element| mapper(element)), - ) + Box::pin(self.recipe.stream(input).map(mapper)) } } diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 421fb917..9fe110b0 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,6 +1,9 @@ use crate::{BoxFuture, MaybeSend, Subscription}; -use futures::{channel::mpsc, sink::Sink}; +use futures::{ + channel::mpsc, + sink::{Sink, SinkExt}, +}; use std::{collections::HashMap, marker::PhantomData}; /// A registry of subscription streams. @@ -64,7 +67,7 @@ where + MaybeSend + Clone, { - use futures::{future::FutureExt, stream::StreamExt}; + use futures::stream::StreamExt; let mut futures: Vec<BoxFuture<()>> = Vec::new(); @@ -85,19 +88,29 @@ where continue; } - let (cancel, cancelled) = futures::channel::oneshot::channel(); + let (cancel, mut canceled) = futures::channel::oneshot::channel(); // TODO: Use bus if/when it supports async let (event_sender, event_receiver) = futures::channel::mpsc::channel(100); - let stream = recipe.stream(event_receiver.boxed()); - - let future = futures::future::select( - cancelled, - stream.map(Ok).forward(receiver.clone()), - ) - .map(|_| ()); + let mut receiver = receiver.clone(); + let mut stream = recipe.stream(event_receiver.boxed()); + + let future = async move { + loop { + let select = + futures::future::select(&mut canceled, stream.next()); + + match select.await { + futures::future::Either::Left(_) + | futures::future::Either::Right((None, _)) => break, + futures::future::Either::Right((Some(message), _)) => { + let _ = receiver.send(message).await; + } + } + } + }; let _ = self.subscriptions.insert( id, @@ -114,7 +127,7 @@ where futures.push(Box::pin(future)); } - self.subscriptions.retain(|id, _| alive.contains(&id)); + self.subscriptions.retain(|id, _| alive.contains(id)); futures } @@ -143,3 +156,13 @@ where }); } } + +impl<Hasher, Event> Default for Tracker<Hasher, Event> +where + Hasher: std::hash::Hasher + Default, + Event: 'static + Send + Clone, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/glow/Cargo.toml b/glow/Cargo.toml index e0907a66..18215e9b 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "iced_glow" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A glow renderer for iced" license = "MIT AND OFL-1.1" -repository = "https://github.com/hecrj/iced" +repository = "https://github.com/iced-rs/iced" [features] canvas = ["iced_graphics/canvas"] @@ -24,11 +24,11 @@ bytemuck = "1.4" log = "0.4" [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" [dependencies.iced_graphics] -version = "0.2" +version = "0.3" path = "../graphics" features = ["font-fallback", "font-icons", "opengl"] diff --git a/glow/README.md b/glow/README.md index 5e37b7a2..00f38f64 100644 --- a/glow/README.md +++ b/glow/README.md @@ -1,12 +1,12 @@ # `iced_glow` [][documentation] [](https://crates.io/crates/iced_glow) -[](https://github.com/hecrj/iced/blob/master/LICENSE) +[](https://github.com/iced-rs/iced/blob/master/LICENSE) [](https://discord.gg/3xZJ65GAhd) `iced_glow` is a [`glow`] renderer for [`iced_native`]. This renderer supports OpenGL 3.0+ and OpenGL ES 2.0. -This is renderer is mostly used as a fallback for hardware that doesn't support [`wgpu`] (Vulkan, Metal or DX12). +This renderer is mostly used as a fallback for hardware that doesn't support [`wgpu`] (Vulkan, Metal or DX12). Currently, `iced_glow` supports the following primitives: - Text, which is rendered using [`glow_glyph`]. No shaping at all. @@ -34,7 +34,7 @@ iced_glow = "0.2" __Iced moves fast and the `master` branch can contain breaking changes!__ If you want to learn about a specific release, check out [the release list]. -[the release list]: https://github.com/hecrj/iced/releases +[the release list]: https://github.com/iced-rs/iced/releases ## Current limitations diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 89dc1aaa..78d4229e 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -13,7 +13,7 @@ use iced_native::{Font, Size}; /// A [`glow`] graphics backend for [`iced`]. /// /// [`glow`]: https://github.com/grovesNL/glow -/// [`iced`]: https://github.com/hecrj/iced +/// [`iced`]: https://github.com/iced-rs/iced #[derive(Debug)] pub struct Backend { quad_pipeline: quad::Pipeline, @@ -173,7 +173,6 @@ impl Backend { glow_glyph::VerticalAlign::Bottom } }), - ..Default::default() }; self.text_pipeline.queue(text); diff --git a/glow/src/lib.rs b/glow/src/lib.rs index 4e5a75d7..de9c0002 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -1,16 +1,24 @@ //! A [`glow`] renderer for [`iced_native`]. //! -//!  +//!  //! //! [`glow`]: https://github.com/grovesNL/glow -//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] #![forbid(rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] pub use glow; @@ -22,7 +30,6 @@ mod text; mod triangle; pub mod settings; -pub mod widget; pub mod window; pub use backend::Backend; @@ -30,10 +37,8 @@ pub use settings::Settings; pub(crate) use iced_graphics::Transformation; -#[doc(no_inline)] -pub use widget::*; - pub use iced_graphics::{Error, Viewport}; +pub use iced_native::Theme; pub use iced_native::alignment; pub use iced_native::{Alignment, Background, Color, Command, Length, Vector}; @@ -41,5 +46,6 @@ pub use iced_native::{Alignment, Background, Color, Command, Length, Vector}; /// A [`glow`] graphics renderer for [`iced`]. /// /// [`glow`]: https://github.com/grovesNL/glow -/// [`iced`]: https://github.com/hecrj/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +/// [`iced`]: https://github.com/iced-rs/iced +pub type Renderer<Theme = iced_native::Theme> = + iced_graphics::Renderer<Backend, Theme>; diff --git a/glow/src/program.rs b/glow/src/program.rs index 9a02d578..1eb9c535 100644 --- a/glow/src/program.rs +++ b/glow/src/program.rs @@ -70,7 +70,7 @@ impl Shader { unsafe { let shader = gl.create_shader(stage).expect("Cannot create shader"); - gl.shader_source(shader, &content); + gl.shader_source(shader, content); gl.compile_shader(shader); if !gl.get_shader_compile_status(shader) { diff --git a/glow/src/quad/compatibility.rs b/glow/src/quad/compatibility.rs index 76f98ab7..eb3fb7e0 100644 --- a/glow/src/quad/compatibility.rs +++ b/glow/src/quad/compatibility.rs @@ -110,22 +110,13 @@ impl Pipeline { bounds: Rectangle<u32>, ) { // TODO: Remove this allocation (probably by changing the shader and removing the need of two `position`) - let vertices: Vec<Vertex> = instances - .iter() - .flat_map(|quad| Vertex::from_quad(quad)) - .collect(); + let vertices: Vec<Vertex> = + instances.iter().flat_map(Vertex::from_quad).collect(); // TODO: Remove this allocation (or allocate only when needed) let indices: Vec<i32> = (0..instances.len().min(MAX_QUADS) as i32) .flat_map(|i| { - [ - 0 + i * 4, - 1 + i * 4, - 2 + i * 4, - 2 + i * 4, - 1 + i * 4, - 3 + i * 4, - ] + [i * 4, 1 + i * 4, 2 + i * 4, 2 + i * 4, 1 + i * 4, 3 + i * 4] }) .cycle() .take(instances.len() * 6) @@ -187,13 +178,13 @@ impl Pipeline { gl.buffer_sub_data_u8_slice( glow::ARRAY_BUFFER, 0, - bytemuck::cast_slice(&vertices), + bytemuck::cast_slice(vertices), ); gl.buffer_sub_data_u8_slice( glow::ELEMENT_ARRAY_BUFFER, 0, - bytemuck::cast_slice(&indices), + bytemuck::cast_slice(indices), ); gl.draw_elements( diff --git a/glow/src/quad/core.rs b/glow/src/quad/core.rs index f37300f6..3e51b1ca 100644 --- a/glow/src/quad/core.rs +++ b/glow/src/quad/core.rs @@ -154,7 +154,7 @@ impl Pipeline { gl.buffer_sub_data_u8_slice( glow::ARRAY_BUFFER, 0, - bytemuck::cast_slice(&instances), + bytemuck::cast_slice(instances), ); gl.draw_arrays_instanced( diff --git a/glow/src/settings.rs b/glow/src/settings.rs index f3dddfaf..3691747b 100644 --- a/glow/src/settings.rs +++ b/glow/src/settings.rs @@ -4,7 +4,7 @@ pub use iced_graphics::Antialiasing; /// The settings of a [`Backend`]. /// /// [`Backend`]: crate::Backend -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct Settings { /// The bytes of the font that will be used by default. /// @@ -39,6 +39,18 @@ impl Default for Settings { } } +impl std::fmt::Debug for Settings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Settings") + // Instead of printing the font bytes, we simply show a `bool` indicating if using a default font or not. + .field("default_font", &self.default_font.is_none()) + .field("default_text_size", &self.default_text_size) + .field("text_multithreading", &self.text_multithreading) + .field("antialiasing", &self.antialiasing) + .finish() + } +} + impl Settings { /// Creates new [`Settings`] using environment configuration. /// diff --git a/glow/src/text.rs b/glow/src/text.rs index 0d45d61b..37ccdece 100644 --- a/glow/src/text.rs +++ b/glow/src/text.rs @@ -54,7 +54,7 @@ impl Pipeline { #[cfg(target_arch = "wasm32")] let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true); - let draw_brush = draw_brush_builder.build(&gl); + let draw_brush = draw_brush_builder.build(gl); let measure_brush = glyph_brush::GlyphBrushBuilder::using_font(font).build(); @@ -180,7 +180,8 @@ impl Pipeline { } b_count += utf8_len; } - return byte_index; + + byte_index }; if !nearest_only { diff --git a/glow/src/widget.rs b/glow/src/widget.rs deleted file mode 100644 index ee2810f9..00000000 --- a/glow/src/widget.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_glow::{button, Button}; -//! ``` -use crate::Renderer; - -pub mod button; -pub mod checkbox; -pub mod container; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod radio; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -pub use container::Container; -#[doc(no_inline)] -pub use pane_grid::PaneGrid; -#[doc(no_inline)] -pub use pick_list::PickList; -#[doc(no_inline)] -pub use progress_bar::ProgressBar; -#[doc(no_inline)] -pub use radio::Radio; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - -pub use iced_native::widget::{Image, Space}; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message> = - iced_native::widget::Column<'a, Message, Renderer>; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message> = iced_native::widget::Row<'a, Message, Renderer>; - -/// A paragraph of text. -pub type Text = iced_native::widget::Text<Renderer>; diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs deleted file mode 100644 index f11ff25e..00000000 --- a/glow/src/widget/button.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::button::{Style, StyleSheet}; -pub use iced_native::widget::button::State; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message> = - iced_native::widget::Button<'a, Message, Renderer>; diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs deleted file mode 100644 index 399dd19c..00000000 --- a/glow/src/widget/canvas.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub use iced_graphics::canvas::*; diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs deleted file mode 100644 index 76d572d9..00000000 --- a/glow/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_graphics::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message> = - iced_native::widget::Checkbox<'a, Message, Renderer>; diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs deleted file mode 100644 index c16db50d..00000000 --- a/glow/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_graphics::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message> = - iced_native::widget::Container<'a, Message, Renderer>; diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs deleted file mode 100644 index 3c47acf0..00000000 --- a/glow/src/widget/pane_grid.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_graphics::pane_grid::{ - Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent, - Split, State, StyleSheet, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message> = - iced_native::widget::PaneGrid<'a, Message, Renderer>; - -/// The content of a [`Pane`]. -pub type Content<'a, Message> = - iced_native::widget::pane_grid::Content<'a, Message, Renderer>; - -/// The title bar of a [`Pane`]. -pub type TitleBar<'a, Message> = - iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/glow/src/widget/pick_list.rs b/glow/src/widget/pick_list.rs deleted file mode 100644 index 4d93be68..00000000 --- a/glow/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -pub use iced_native::widget::pick_list::State; - -pub use iced_graphics::overlay::menu::Style as Menu; -pub use iced_graphics::pick_list::{Style, StyleSheet}; - -/// A widget allowing the selection of a single value from a list of options. -pub type PickList<'a, T, Message> = - iced_native::widget::PickList<'a, T, Message, crate::Renderer>; diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs deleted file mode 100644 index 413e6fb7..00000000 --- a/glow/src/widget/progress_bar.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. - -pub use iced_graphics::progress_bar::*; diff --git a/glow/src/widget/qr_code.rs b/glow/src/widget/qr_code.rs deleted file mode 100644 index 7b1c2408..00000000 --- a/glow/src/widget/qr_code.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Encode and display information in a QR code. -pub use iced_graphics::qr_code::*; diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs deleted file mode 100644 index 9ef1d7a5..00000000 --- a/glow/src/widget/radio.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_graphics::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message> = iced_native::widget::Radio<'a, Message, Renderer>; diff --git a/glow/src/widget/rule.rs b/glow/src/widget/rule.rs deleted file mode 100644 index 40281773..00000000 --- a/glow/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_graphics::rule::*; diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs deleted file mode 100644 index d5635ec5..00000000 --- a/glow/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; -pub use iced_native::widget::scrollable::State; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message> = - iced_native::widget::Scrollable<'a, Message, Renderer>; diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs deleted file mode 100644 index 2fb3d5d9..00000000 --- a/glow/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; -pub use iced_native::widget::slider::{Slider, State}; diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs deleted file mode 100644 index 5560e3e0..00000000 --- a/glow/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::text_input::{Style, StyleSheet}; -pub use iced_native::widget::text_input::State; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message> = - iced_native::widget::TextInput<'a, Message, Renderer>; diff --git a/glow/src/widget/toggler.rs b/glow/src/widget/toggler.rs deleted file mode 100644 index 40379025..00000000 --- a/glow/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_graphics::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message> = - iced_native::widget::Toggler<'a, Message, Renderer>; diff --git a/glow/src/widget/tooltip.rs b/glow/src/widget/tooltip.rs deleted file mode 100644 index c6af3903..00000000 --- a/glow/src/widget/tooltip.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Display a widget over another. -/// A widget allowing the selection of a single value from a list of options. -pub type Tooltip<'a, Message> = - iced_native::widget::Tooltip<'a, Message, crate::Renderer>; - -pub use iced_native::widget::tooltip::Position; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs index 44019fb2..f6afaa68 100644 --- a/glow/src/window/compositor.rs +++ b/glow/src/window/compositor.rs @@ -1,18 +1,21 @@ use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; -use core::ffi::c_void; use glow::HasContext; -use iced_graphics::{Antialiasing, Size}; +use iced_graphics::{compositor, Antialiasing, Size}; + +use core::ffi::c_void; +use std::marker::PhantomData; /// A window graphics backend for iced powered by `glow`. #[allow(missing_debug_implementations)] -pub struct Compositor { +pub struct Compositor<Theme> { gl: glow::Context, + theme: PhantomData<Theme>, } -impl iced_graphics::window::GLCompositor for Compositor { +impl<Theme> iced_graphics::window::GLCompositor for Compositor<Theme> { type Settings = Settings; - type Renderer = Renderer; + type Renderer = Renderer<Theme>; unsafe fn new( settings: Self::Settings, @@ -20,6 +23,8 @@ impl iced_graphics::window::GLCompositor for Compositor { ) -> Result<(Self, Self::Renderer), Error> { let gl = glow::Context::from_loader_function(loader_function); + log::info!("{:#?}", settings); + let version = gl.version(); log::info!("Version: {:?}", version); log::info!("Embedded: {}", version.is_embedded); @@ -44,7 +49,13 @@ impl iced_graphics::window::GLCompositor for Compositor { let renderer = Renderer::new(Backend::new(&gl, settings)); - Ok((Self { gl }, renderer)) + Ok(( + Self { + gl, + theme: PhantomData, + }, + renderer, + )) } fn sample_count(settings: &Settings) -> u32 { @@ -65,6 +76,15 @@ impl iced_graphics::window::GLCompositor for Compositor { } } + fn fetch_information(&self) -> compositor::Information { + let adapter = unsafe { self.gl.get_parameter_string(glow::RENDERER) }; + + compositor::Information { + backend: format!("{:?}", self.gl.version()), + adapter, + } + } + fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index d1b0468d..d84f9d70 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glutin" -version = "0.2.0" +version = "0.3.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A glutin runtime for Iced" @@ -12,21 +12,25 @@ categories = ["gui"] [features] debug = ["iced_winit/debug"] +system = ["iced_winit/system"] + +[dependencies.log] +version = "0.4" [dependencies.glutin] -version = "0.28" +version = "0.29" git = "https://github.com/iced-rs/glutin" -rev = "7a0ee02782eb2bf059095e0c953c4bb53f1eef0e" +rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" [dependencies.iced_winit] -version = "0.3" +version = "0.4" path = "../winit" [dependencies.iced_graphics] -version = "0.2" +version = "0.3" path = "../graphics" features = ["opengl"] diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 27a932fc..f474cbba 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -2,6 +2,7 @@ use crate::mouse; use crate::{Error, Executor, Runtime}; +pub use iced_winit::application::StyleSheet; pub use iced_winit::Application; use iced_graphics::window; @@ -9,8 +10,9 @@ use iced_winit::application; use iced_winit::conversion; use iced_winit::futures; use iced_winit::futures::channel::mpsc; +use iced_winit::renderer; use iced_winit::user_interface; -use iced_winit::{Clipboard, Debug, Proxy, Settings}; +use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; use glutin::window::Window; use std::mem::ManuallyDrop; @@ -25,20 +27,21 @@ where A: Application + 'static, E: Executor + 'static, C: window::GLCompositor<Renderer = A::Renderer> + 'static, + <A::Renderer as iced_native::Renderer>::Theme: StyleSheet, { use futures::task; use futures::Future; - use glutin::event_loop::EventLoop; + use glutin::event_loop::EventLoopBuilder; use glutin::platform::run_return::EventLoopExtRunReturn; use glutin::ContextBuilder; let mut debug = Debug::new(); debug.startup_started(); - let mut event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let mut event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let executor = E::new().map_err(Error::ExecutorCreationFailed)?; let proxy = Proxy::new(event_loop.create_proxy()); @@ -51,16 +54,15 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); - let context = { let builder = settings.window.into_builder( &application.title(), - application.mode(), event_loop.primary_monitor(), settings.id, ); + log::info!("Window builder: {:#?}", builder); + let opengl_builder = ContextBuilder::new() .with_vsync(true) .with_multisampling(C::sample_count(&compositor_settings) as u16); @@ -75,17 +77,35 @@ where (opengl_builder, opengles_builder) }; + log::info!("Trying first builder: {:#?}", first_builder); + let context = first_builder .build_windowed(builder.clone(), &event_loop) - .or_else(|_| second_builder.build_windowed(builder, &event_loop)) + .or_else(|_| { + log::info!("Trying second builder: {:#?}", second_builder); + second_builder.build_windowed(builder, &event_loop) + }) .map_err(|error| { use glutin::CreationError; + use iced_graphics::Error as ContextError; match error { CreationError::Window(error) => { Error::WindowCreationFailed(error) } - _ => Error::GraphicsAdapterNotFound, + CreationError::OpenGlVersionNotSupported => { + Error::GraphicsCreationFailed( + ContextError::VersionNotSupported, + ) + } + CreationError::NoAvailablePixelFormat => { + Error::GraphicsCreationFailed( + ContextError::NoAvailablePixelFormat, + ) + } + error => Error::GraphicsCreationFailed( + ContextError::BackendError(error.to_string()), + ), } })?; @@ -102,17 +122,6 @@ where })? }; - let mut clipboard = Clipboard::connect(context.window()); - - application::run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - context.window(), - ); - runtime.track(subscription); - let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::<A, E, C>( @@ -120,20 +129,20 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, context, + init_command, settings.exit_on_close_request, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); - event_loop.run_return(move |event, _, control_flow| { + let _ = event_loop.run_return(move |event, _, control_flow| { use glutin::event_loop::ControlFlow; - if let ControlFlow::Exit = control_flow { + if let ControlFlow::ExitWithCode(_) = control_flow { return; } @@ -172,25 +181,44 @@ async fn run_instance<A, E, C>( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut clipboard: Clipboard, mut proxy: glutin::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver<glutin::event::Event<'_, A::Message>>, mut context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>, + init_command: Command<A::Message>, exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, C: window::GLCompositor<Renderer = A::Renderer> + 'static, + <A::Renderer as iced_native::Renderer>::Theme: StyleSheet, { use glutin::event; use iced_winit::futures::stream::StreamExt; + let mut clipboard = Clipboard::connect(context.window()); + let mut cache = user_interface::Cache::default(); let mut state = application::State::new(&application, context.window()); let mut viewport_version = state.viewport_version(); + + application::run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + context.window(), + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, user_interface::Cache::default(), &mut renderer, state.logical_size(), @@ -232,18 +260,22 @@ async fn run_instance<A, E, C>( user_interface::State::Outdated ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application application::update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, &mut debug, &mut messages, context.window(), + || compositor.fetch_information(), ); // Update window @@ -253,7 +285,7 @@ async fn run_instance<A, E, C>( user_interface = ManuallyDrop::new(application::build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), @@ -266,8 +298,14 @@ async fn run_instance<A, E, C>( } debug.draw_started(); - let new_mouse_interaction = - user_interface.draw(&mut renderer, state.cursor_position()); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -319,8 +357,14 @@ async fn run_instance<A, E, C>( debug.layout_finished(); debug.draw_started(); - let new_mouse_interaction = user_interface - .draw(&mut renderer, state.cursor_position()); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); debug.draw_finished(); if new_mouse_interaction != mouse_interaction { diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs index 72397791..33afd664 100644 --- a/glutin/src/lib.rs +++ b/glutin/src/lib.rs @@ -7,27 +7,27 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![deny(unsafe_code)] +#![deny( + missing_docs, + missing_debug_implementations, + unsafe_code, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] #![forbid(rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_cfg))] pub use glutin; + #[doc(no_inline)] -pub use iced_native::*; +pub use iced_winit::*; pub mod application; -pub use iced_winit::clipboard; -pub use iced_winit::conversion; -pub use iced_winit::settings; -pub use iced_winit::window; -pub use iced_winit::{Error, Mode}; - #[doc(no_inline)] pub use application::Application; -#[doc(no_inline)] -pub use clipboard::Clipboard; -#[doc(no_inline)] -pub use settings::Settings; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 8ccc7849..49d4d9c6 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "iced_graphics" -version = "0.2.0" +version = "0.3.1" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" license = "MIT" -repository = "https://github.com/hecrj/iced" +repository = "https://github.com/iced-rs/iced" documentation = "https://docs.rs/iced_graphics" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] @@ -28,15 +28,15 @@ version = "1.4" features = ["derive"] [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" [dependencies.iced_style] -version = "0.3" +version = "0.4" path = "../style" [dependencies.lyon] -version = "0.17" +version = "1.0" optional = true [dependencies.qrcode] diff --git a/graphics/src/error.rs b/graphics/src/error.rs index c86e326a..77758f54 100644 --- a/graphics/src/error.rs +++ b/graphics/src/error.rs @@ -1,7 +1,19 @@ -/// A graphical error that occurred while running an application. +/// An error that occurred while creating an application's graphical context. #[derive(Debug, thiserror::Error)] pub enum Error { - /// A suitable graphics adapter or device could not be found + /// The requested backend version is not supported. + #[error("the requested backend version is not supported")] + VersionNotSupported, + + /// Failed to find any pixel format that matches the criteria. + #[error("failed to find any pixel format that matches the criteria")] + NoAvailablePixelFormat, + + /// A suitable graphics adapter or device could not be found. #[error("a suitable graphics adapter or device could not be found")] - AdapterNotFound, + GraphicsAdapterNotFound, + + /// An error occured in the context's internal backend + #[error("an error occured in the context's internal backend")] + BackendError(String), } diff --git a/graphics/src/font/source.rs b/graphics/src/font/source.rs index a2d3f51d..c0b50e1d 100644 --- a/graphics/src/font/source.rs +++ b/graphics/src/font/source.rs @@ -37,3 +37,9 @@ impl Source { } } } + +impl Default for Source { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 7a32c850..af545713 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -1,12 +1,13 @@ //! Organize rendering primitives into a flattened list of layers. use crate::alignment; -use crate::image; -use crate::svg; use crate::triangle; use crate::{ Background, Font, Point, Primitive, Rectangle, Size, Vector, Viewport, }; +use iced_native::image; +use iced_native::svg; + /// A group of primitives that should be clipped together. #[derive(Debug, Clone)] pub struct Layer<'a> { @@ -201,7 +202,7 @@ impl<'a> Layer<'a> { Self::process_primitive( layers, translation + *new_translation, - &content, + content, current_layer, ); } @@ -209,7 +210,7 @@ impl<'a> Layer<'a> { Self::process_primitive( layers, translation, - &cache, + cache, current_layer, ); } diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index b3be62af..11082472 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -1,17 +1,25 @@ //! A bunch of backend-agnostic types that can be leveraged to build a renderer //! for [`iced`]. //! -//!  +//!  //! -//! [`iced`]: https://github.com/hecrj/iced +//! [`iced`]: https://github.com/iced-rs/iced #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![deny(unsafe_code)] +#![deny( + missing_debug_implementations, + missing_docs, + unsafe_code, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] #![forbid(rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] mod antialiasing; mod error; @@ -28,9 +36,6 @@ pub mod triangle; pub mod widget; pub mod window; -#[doc(no_inline)] -pub use widget::*; - pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; @@ -39,6 +44,7 @@ pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; +pub use window::compositor; pub use iced_native::alignment; pub use iced_native::{ diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs index c5ff093d..8b489e5e 100644 --- a/graphics/src/overlay/menu.rs +++ b/graphics/src/overlay/menu.rs @@ -1,3 +1,3 @@ //! Build and show dropdown menus. -pub use iced_style::menu::Style; +pub use iced_style::menu::{Appearance, StyleSheet}; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index c32eb471..cdbc4f40 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,26 +1,32 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; use crate::{Primitive, Vector}; +use iced_native::image; use iced_native::layout; use iced_native::renderer; +use iced_native::svg; use iced_native::text::{self, Text}; use iced_native::{Background, Element, Font, Point, Rectangle, Size}; pub use iced_native::renderer::Style; +use std::marker::PhantomData; + /// A backend-agnostic renderer that supports all the built-in widgets. #[derive(Debug)] -pub struct Renderer<B: Backend> { +pub struct Renderer<B: Backend, Theme> { backend: B, primitives: Vec<Primitive>, + theme: PhantomData<Theme>, } -impl<B: Backend> Renderer<B> { +impl<B: Backend, T> Renderer<B, T> { /// Creates a new [`Renderer`] from the given [`Backend`]. pub fn new(backend: B) -> Self { Self { backend, primitives: Vec::new(), + theme: PhantomData, } } @@ -41,16 +47,18 @@ impl<B: Backend> Renderer<B> { } } -impl<B> iced_native::Renderer for Renderer<B> +impl<B, T> iced_native::Renderer for Renderer<B, T> where B: Backend, { + type Theme = T; + fn layout<'a, Message>( &mut self, element: &Element<'a, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - let layout = element.layout(self, limits); + let layout = element.as_widget().layout(self, limits); self.backend.trim_measurements(); @@ -112,7 +120,7 @@ where } } -impl<B> text::Renderer for Renderer<B> +impl<B, T> text::Renderer for Renderer<B, T> where B: Backend + backend::Text, { @@ -168,3 +176,31 @@ where }); } } + +impl<B, T> image::Renderer for Renderer<B, T> +where + B: Backend + backend::Image, +{ + type Handle = image::Handle; + + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.backend().dimensions(handle) + } + + fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { + self.draw_primitive(Primitive::Image { handle, bounds }) + } +} + +impl<B, T> svg::Renderer for Renderer<B, T> +where + B: Backend + backend::Svg, +{ + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.backend().viewport_dimensions(handle) + } + + fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { + self.draw_primitive(Primitive::Svg { handle, bounds }) + } +} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index e34d267f..e7fab97c 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -1,67 +1,4 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_graphics::{button, Button}; -//! ``` -pub mod button; -pub mod checkbox; -pub mod container; -pub mod image; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod radio; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod svg; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -mod column; -mod row; -mod space; -mod text; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -pub use container::Container; -#[doc(no_inline)] -pub use pane_grid::PaneGrid; -#[doc(no_inline)] -pub use pick_list::PickList; -#[doc(no_inline)] -pub use progress_bar::ProgressBar; -#[doc(no_inline)] -pub use radio::Radio; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -pub use column::Column; -pub use image::Image; -pub use row::Row; -pub use space::Space; -pub use svg::Svg; -pub use text::Text; - +//! Use the graphical widgets supported out-of-the-box. #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub mod canvas; diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs deleted file mode 100644 index 7b40c47b..00000000 --- a/graphics/src/widget/button.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_native::widget::button::{State, Style, StyleSheet}; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message, Backend> = - iced_native::widget::Button<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index 65d7e37e..88403fd7 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -3,16 +3,6 @@ //! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a //! [`Frame`]. It can be used for animation, data visualization, game graphics, //! and more! -use crate::renderer::{self, Renderer}; -use crate::{Backend, Primitive}; - -use iced_native::layout; -use iced_native::mouse; -use iced_native::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, - Widget, -}; -use std::marker::PhantomData; pub mod event; pub mod path; @@ -37,34 +27,32 @@ pub use program::Program; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use text::Text; +use crate::{Backend, Primitive, Renderer}; + +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; +use iced_native::{ + Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, +}; + +use std::marker::PhantomData; + /// A widget capable of drawing 2D graphics. /// -/// # Examples -/// The repository has a couple of [examples] showcasing how to use a -/// [`Canvas`]: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`game_of_life`], an interactive version of the Game of Life, invented by -/// John Conway. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// -/// [examples]: https://github.com/hecrj/iced/tree/master/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock -/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life -/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system -/// /// ## Drawing a simple circle /// If you want to get a quick overview, here's how we can draw a simple circle: /// /// ```no_run /// # mod iced { -/// # pub use iced_graphics::canvas; -/// # pub use iced_native::{Color, Rectangle}; +/// # pub mod widget { +/// # pub use iced_graphics::widget::canvas; +/// # } +/// # pub use iced_native::{Color, Rectangle, Theme}; /// # } -/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle}; +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme}; /// /// // First, we define the data we need for drawing /// #[derive(Debug)] @@ -74,7 +62,9 @@ pub use text::Text; /// /// // Then, we implement the `Program` trait /// impl Program<()> for Circle { -/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// type State = (); +/// +/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ /// // We prepare a new `Frame` /// let mut frame = Frame::new(bounds.size()); /// @@ -93,14 +83,21 @@ pub use text::Text; /// let canvas = Canvas::new(Circle { radius: 50.0 }); /// ``` #[derive(Debug)] -pub struct Canvas<Message, P: Program<Message>> { +pub struct Canvas<Message, Theme, P> +where + P: Program<Message, Theme>, +{ width: Length, height: Length, program: P, - phantom: PhantomData<Message>, + message_: PhantomData<Message>, + theme_: PhantomData<Theme>, } -impl<Message, P: Program<Message>> Canvas<Message, P> { +impl<Message, Theme, P> Canvas<Message, Theme, P> +where + P: Program<Message, Theme>, +{ const DEFAULT_SIZE: u16 = 100; /// Creates a new [`Canvas`]. @@ -109,7 +106,8 @@ impl<Message, P: Program<Message>> Canvas<Message, P> { width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), program, - phantom: PhantomData, + message_: PhantomData, + theme_: PhantomData, } } @@ -126,11 +124,20 @@ impl<Message, P: Program<Message>> Canvas<Message, P> { } } -impl<Message, P, B> Widget<Message, Renderer<B>> for Canvas<Message, P> +impl<Message, P, B, T> Widget<Message, Renderer<B, T>> for Canvas<Message, T, P> where - P: Program<Message>, + P: Program<Message, T>, B: Backend, { + 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 width(&self) -> Length { self.width } @@ -141,7 +148,7 @@ where fn layout( &self, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.width).height(self.height); @@ -152,10 +159,11 @@ where fn on_event( &mut self, + tree: &mut Tree, event: iced_native::Event, layout: Layout<'_>, cursor_position: Point, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { @@ -174,8 +182,10 @@ where let cursor = Cursor::from_window_position(cursor_position); if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::<P::State>(); + let (event_status, message) = - self.program.update(canvas_event, bounds, cursor); + self.program.update(state, canvas_event, bounds, cursor); if let Some(message) = message { shell.publish(message); @@ -189,20 +199,24 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, ) -> mouse::Interaction { let bounds = layout.bounds(); let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); - self.program.mouse_interaction(bounds, cursor) + self.program.mouse_interaction(state, bounds, cursor) } fn draw( &self, - renderer: &mut Renderer<B>, + tree: &Tree, + renderer: &mut Renderer<B, T>, + theme: &T, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -218,12 +232,13 @@ where let translation = Vector::new(bounds.x, bounds.y); let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); renderer.with_translation(translation, |renderer| { renderer.draw_primitive(Primitive::Group { primitives: self .program - .draw(bounds, cursor) + .draw(state, theme, bounds, cursor) .into_iter() .map(Geometry::into_primitive) .collect(), @@ -232,14 +247,17 @@ where } } -impl<'a, Message, P, B> From<Canvas<Message, P>> - for Element<'a, Message, Renderer<B>> +impl<'a, Message, P, B, T> From<Canvas<Message, T, P>> + for Element<'a, Message, Renderer<B, T>> where - Message: 'static, - P: Program<Message> + 'a, + Message: 'a, + P: Program<Message, T> + 'a, B: Backend, + T: 'a, { - fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer<B>> { + fn from( + canvas: Canvas<Message, T, P>, + ) -> Element<'a, Message, Renderer<B, T>> { Element::new(canvas) } } diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs index a469417d..49873ac9 100644 --- a/graphics/src/widget/canvas/cache.rs +++ b/graphics/src/widget/canvas/cache.rs @@ -1,7 +1,5 @@ -use crate::{ - canvas::{Frame, Geometry}, - Primitive, -}; +use crate::widget::canvas::{Frame, Geometry}; +use crate::Primitive; use iced_native::Size; use std::{cell::RefCell, sync::Arc}; @@ -37,7 +35,7 @@ impl Cache { } /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&mut self) { + pub fn clear(&self) { *self.state.borrow_mut() = State::Empty; } diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs index 357dfa62..516539ca 100644 --- a/graphics/src/widget/canvas/frame.rs +++ b/graphics/src/widget/canvas/frame.rs @@ -2,11 +2,10 @@ use std::borrow::Cow; use iced_native::{Point, Rectangle, Size, Vector}; -use crate::{ - canvas::path, - canvas::{Fill, Geometry, Path, Stroke, Text}, - triangle, Primitive, -}; +use crate::triangle; +use crate::widget::canvas::path; +use crate::widget::canvas::{Fill, Geometry, Path, Stroke, Text}; +use crate::Primitive; use lyon::tessellation; @@ -110,7 +109,7 @@ impl Frame { ) }; - let _ = result.expect("Tessellate path"); + result.expect("Tessellate path"); } /// Draws an axis-aligned rectangle given its top-left corner coordinate and @@ -141,10 +140,9 @@ impl Frame { let options = tessellation::FillOptions::default().with_fill_rule(rule.into()); - let _ = self - .fill_tessellator + self.fill_tessellator .tessellate_rectangle( - &lyon::math::Rect::new(top_left, size.into()), + &lyon::math::Box2D::new(top_left, top_left + size), &options, &mut buffers, ) @@ -189,7 +187,7 @@ impl Frame { ) }; - let _ = result.expect("Stroke path"); + result.expect("Stroke path"); } /// Draws the characters of the given [`Text`] on the [`Frame`], filling @@ -253,6 +251,45 @@ impl Frame { self.transforms.current = self.transforms.previous.pop().unwrap(); } + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + let translation = Vector::new(region.x, region.y); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(region.size()), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + /// Applies a translation to the current transform of the [`Frame`]. #[inline] pub fn translate(&mut self, translation: Vector) { @@ -287,7 +324,13 @@ impl Frame { } /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. - pub fn into_geometry(mut self) -> Geometry { + pub fn into_geometry(self) -> Geometry { + Geometry::from_primitive(Primitive::Group { + primitives: self.into_primitives(), + }) + } + + fn into_primitives(mut self) -> Vec<Primitive> { if !self.buffers.indices.is_empty() { self.primitives.push(Primitive::Mesh2D { buffers: triangle::Mesh2D { @@ -298,9 +341,7 @@ impl Frame { }); } - Geometry::from_primitive(Primitive::Group { - primitives: self.primitives, - }) + self.primitives } } diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs index 8915cda1..e8ac621d 100644 --- a/graphics/src/widget/canvas/geometry.rs +++ b/graphics/src/widget/canvas/geometry.rs @@ -22,9 +22,3 @@ impl Geometry { self.0 } } - -impl From<Geometry> for Primitive { - fn from(geometry: Geometry) -> Primitive { - geometry.0 - } -} diff --git a/graphics/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs index 1728f060..aeb2589e 100644 --- a/graphics/src/widget/canvas/path.rs +++ b/graphics/src/widget/canvas/path.rs @@ -7,10 +7,10 @@ mod builder; pub use arc::Arc; pub use builder::Builder; -use crate::canvas::LineDash; +use crate::widget::canvas::LineDash; use iced_native::{Point, Size}; -use lyon::algorithms::walk::{walk_along_path, RepeatedPattern}; +use lyon::algorithms::walk::{walk_along_path, RepeatedPattern, WalkerEvent}; use lyon::path::iterator::PathIterator; /// An immutable set of points that may or may not be connected. @@ -73,22 +73,20 @@ impl Path { pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1).then(|| { - [&line_dash.segments[..], &line_dash.segments[..]].concat() - }); + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); let mut draw_line = false; walk_along_path( path.raw().iter().flattened(0.01), 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, &mut RepeatedPattern { - callback: |position: lyon::algorithms::math::Point, - _tangent, - _distance| { + callback: |event: WalkerEvent<'_>| { let point = Point { - x: position.x, - y: position.y, + x: event.position.x, + y: event.position.y, }; if draw_line { @@ -103,8 +101,7 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { }, index: line_dash.offset, intervals: segments_odd - .as_ref() - .map(Vec::as_slice) + .as_deref() .unwrap_or(line_dash.segments), }, ); diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs index d04dbdde..5121aa68 100644 --- a/graphics/src/widget/canvas/path/builder.rs +++ b/graphics/src/widget/canvas/path/builder.rs @@ -1,4 +1,4 @@ -use crate::canvas::path::{arc, Arc, Path}; +use crate::widget::canvas::path::{arc, Arc, Path}; use iced_native::{Point, Size}; use lyon::path::builder::SvgPathBuilder; @@ -8,7 +8,7 @@ use lyon::path::builder::SvgPathBuilder; /// Once a [`Path`] is built, it can no longer be mutated. #[allow(missing_debug_implementations)] pub struct Builder { - raw: lyon::path::builder::WithSvg<lyon::path::path::Builder>, + raw: lyon::path::builder::WithSvg<lyon::path::path::BuilderImpl>, } impl Builder { @@ -42,22 +42,61 @@ impl Builder { /// Adds a circular arc to the [`Path`] with the given control points and /// radius. /// - /// The arc is connected to the previous point by a straight line, if - /// necessary. + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { use lyon::{math, path}; - let a = math::Point::new(a.x, a.y); + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); - if self.raw.current_position() != a { - let _ = self.raw.line_to(a); + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; } - let _ = self.raw.arc_to( + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( math::Vector::new(radius, radius), math::Angle::radians(0.0), - path::ArcFlags::default(), - math::Point::new(b.x, b.y), + path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, ); } @@ -151,3 +190,9 @@ impl Builder { } } } + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs index 85a2f67b..656dbfa6 100644 --- a/graphics/src/widget/canvas/program.rs +++ b/graphics/src/widget/canvas/program.rs @@ -1,6 +1,7 @@ -use crate::canvas::event::{self, Event}; -use crate::canvas::{Cursor, Geometry}; -use iced_native::{mouse, Rectangle}; +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::{Cursor, Geometry}; +use crate::Rectangle; /// The state and logic of a [`Canvas`]. /// @@ -8,8 +9,11 @@ use iced_native::{mouse, Rectangle}; /// application. /// /// [`Canvas`]: crate::widget::Canvas -pub trait Program<Message> { - /// Updates the state of the [`Program`]. +pub trait Program<Message, Theme = iced_native::Theme> { + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. /// /// When a [`Program`] is used in a [`Canvas`], the runtime will call this /// method for each [`Event`]. @@ -21,7 +25,8 @@ pub trait Program<Message> { /// /// [`Canvas`]: crate::widget::Canvas fn update( - &mut self, + &self, + _state: &mut Self::State, _event: Event, _bounds: Rectangle, _cursor: Cursor, @@ -36,7 +41,13 @@ pub trait Program<Message> { /// /// [`Frame`]: crate::widget::canvas::Frame /// [`Cache`]: crate::widget::canvas::Cache - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>; + fn draw( + &self, + state: &Self::State, + theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -46,6 +57,7 @@ pub trait Program<Message> { /// [`Canvas`]: crate::widget::Canvas fn mouse_interaction( &self, + _state: &Self::State, _bounds: Rectangle, _cursor: Cursor, ) -> mouse::Interaction { @@ -53,28 +65,38 @@ pub trait Program<Message> { } } -impl<T, Message> Program<Message> for &mut T +impl<Message, Theme, T> Program<Message, Theme> for &T where - T: Program<Message>, + T: Program<Message, Theme>, { + type State = T::State; + fn update( - &mut self, + &self, + state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option<Message>) { - T::update(self, event, bounds, cursor) + T::update(self, state, event, bounds, cursor) } - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { - T::draw(self, bounds, cursor) + fn draw( + &self, + state: &Self::State, + theme: &Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Geometry> { + T::draw(self, state, theme, bounds, cursor) } fn mouse_interaction( &self, + state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> mouse::Interaction { - T::mouse_interaction(self, bounds, cursor) + T::mouse_interaction(self, state, bounds, cursor) } } diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs index ab070a70..056f8204 100644 --- a/graphics/src/widget/canvas/text.rs +++ b/graphics/src/widget/canvas/text.rs @@ -6,7 +6,14 @@ use crate::{Color, Font, Point}; pub struct Text { /// The contents of the text pub content: String, - /// The position where to begin drawing the text (top-left corner coordinates) + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. pub position: Point, /// The color of the text pub color: Color, diff --git a/graphics/src/widget/checkbox.rs b/graphics/src/widget/checkbox.rs deleted file mode 100644 index 0d2e93f9..00000000 --- a/graphics/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_style::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message, Backend> = - iced_native::widget::Checkbox<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs deleted file mode 100644 index 561681d5..00000000 --- a/graphics/src/widget/column.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::Renderer; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message, Backend> = - iced_native::widget::Column<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs deleted file mode 100644 index 99996f3b..00000000 --- a/graphics/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_style::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message, Backend> = - iced_native::widget::Container<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs deleted file mode 100644 index 76152484..00000000 --- a/graphics/src/widget/image.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Display images in your user interface. -pub mod viewer; - -use crate::backend::{self, Backend}; -use crate::{Primitive, Rectangle, Renderer}; - -use iced_native::image; - -pub use iced_native::widget::image::{Image, Viewer}; -pub use image::Handle; - -impl<B> image::Renderer for Renderer<B> -where - B: Backend + backend::Image, -{ - type Handle = image::Handle; - - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { - self.backend().dimensions(handle) - } - - fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Image { handle, bounds }) - } -} diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs deleted file mode 100644 index 9260990a..00000000 --- a/graphics/src/widget/image/viewer.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Zoom and pan on an image. -pub use iced_native::widget::image::Viewer; diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs deleted file mode 100644 index 95189920..00000000 --- a/graphics/src/widget/pane_grid.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_native::widget::pane_grid::{ - Axis, Configuration, Content, Direction, DragEvent, Node, Pane, - ResizeEvent, Split, State, TitleBar, -}; - -pub use iced_style::pane_grid::{Line, StyleSheet}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message, Backend> = - iced_native::widget::PaneGrid<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs deleted file mode 100644 index f3ac12b8..00000000 --- a/graphics/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -use crate::Renderer; - -pub use iced_native::widget::pick_list::State; -pub use iced_style::pick_list::{Style, StyleSheet}; - -/// A widget allowing the selection of a single value from a list of options. -pub type PickList<'a, T, Message, Backend> = - iced_native::widget::PickList<'a, T, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs deleted file mode 100644 index 3666ecfd..00000000 --- a/graphics/src/widget/progress_bar.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. -pub use iced_native::widget::progress_bar::*; diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs index 907794b7..12ce5b1f 100644 --- a/graphics/src/widget/qr_code.rs +++ b/graphics/src/widget/qr_code.rs @@ -1,9 +1,10 @@ //! Encode and display information in a QR code. -use crate::canvas; use crate::renderer::{self, Renderer}; +use crate::widget::canvas; use crate::Backend; use iced_native::layout; +use iced_native::widget::Tree; use iced_native::{ Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; @@ -47,7 +48,7 @@ impl<'a> QRCode<'a> { } } -impl<'a, Message, B> Widget<Message, Renderer<B>> for QRCode<'a> +impl<'a, Message, B, T> Widget<Message, Renderer<B, T>> for QRCode<'a> where B: Backend, { @@ -61,21 +62,20 @@ where fn layout( &self, - _renderer: &Renderer<B>, + _renderer: &Renderer<B, T>, _limits: &layout::Limits, ) -> layout::Node { let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 * f32::from(self.cell_size); - layout::Node::new(Size::new( - f32::from(side_length), - f32::from(side_length), - )) + layout::Node::new(Size::new(side_length, side_length)) } fn draw( &self, - renderer: &mut Renderer<B>, + _state: &Tree, + renderer: &mut Renderer<B, T>, + _theme: &T, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -127,12 +127,13 @@ where } } -impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for QRCode<'a> +impl<'a, Message, B, T> From<QRCode<'a>> + for Element<'a, Message, Renderer<B, T>> where B: Backend, { - fn into(self) -> Element<'a, Message, Renderer<B>> { - Element::new(self) + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) } } diff --git a/graphics/src/widget/radio.rs b/graphics/src/widget/radio.rs deleted file mode 100644 index 20d72747..00000000 --- a/graphics/src/widget/radio.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_style::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message, Backend> = - iced_native::widget::Radio<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs deleted file mode 100644 index 5bee3fd5..00000000 --- a/graphics/src/widget/row.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::Renderer; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message, Backend> = - iced_native::widget::Row<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/rule.rs b/graphics/src/widget/rule.rs deleted file mode 100644 index b96924fa..00000000 --- a/graphics/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_native::widget::rule::*; diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs deleted file mode 100644 index 3fdaf668..00000000 --- a/graphics/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_native::widget::scrollable::State; -pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message, Backend> = - iced_native::widget::Scrollable<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/slider.rs b/graphics/src/widget/slider.rs deleted file mode 100644 index 96dc6ec4..00000000 --- a/graphics/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_native::widget::slider::{Slider, State}; -pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs deleted file mode 100644 index 77e93dbb..00000000 --- a/graphics/src/widget/space.rs +++ /dev/null @@ -1 +0,0 @@ -pub use iced_native::widget::Space; diff --git a/graphics/src/widget/svg.rs b/graphics/src/widget/svg.rs deleted file mode 100644 index 5817a552..00000000 --- a/graphics/src/widget/svg.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Display vector graphics in your application. -use crate::backend::{self, Backend}; -use crate::{Primitive, Rectangle, Renderer}; -use iced_native::svg; - -pub use iced_native::widget::svg::Svg; -pub use svg::Handle; - -impl<B> svg::Renderer for Renderer<B> -where - B: Backend + backend::Svg, -{ - fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { - self.backend().viewport_dimensions(handle) - } - - fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Svg { handle, bounds }) - } -} diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs deleted file mode 100644 index 43516fca..00000000 --- a/graphics/src/widget/text.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Write some text for your users to read. -use crate::Renderer; - -/// A paragraph of text. -/// -/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`. -pub type Text<Backend> = iced_native::widget::Text<Renderer<Backend>>; diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs deleted file mode 100644 index 87384d7e..00000000 --- a/graphics/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_native::widget::text_input::State; -pub use iced_style::text_input::{Style, StyleSheet}; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message, Backend> = - iced_native::widget::TextInput<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs deleted file mode 100644 index 9053e6ed..00000000 --- a/graphics/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_style::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled. -/// -/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message, Backend> = - iced_native::widget::Toggler<'a, Message, Renderer<Backend>>; diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs deleted file mode 100644 index 7dc12ed4..00000000 --- a/graphics/src/widget/tooltip.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` tooltip with a default -/// `Renderer`. -pub type Tooltip<'a, Message, Backend> = - iced_native::widget::Tooltip<'a, Message, Renderer<Backend>>; - -pub use iced_native::widget::tooltip::Position; diff --git a/graphics/src/window.rs b/graphics/src/window.rs index 67ec3322..a38b81f3 100644 --- a/graphics/src/window.rs +++ b/graphics/src/window.rs @@ -1,10 +1,10 @@ //! Draw graphics to window surfaces. -mod compositor; +pub mod compositor; #[cfg(feature = "opengl")] -mod gl_compositor; +pub mod gl_compositor; -pub use compositor::{Compositor, SurfaceError}; +pub use compositor::Compositor; #[cfg(feature = "opengl")] pub use gl_compositor::GLCompositor; diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index 9ea040cd..0c4cadcd 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -1,3 +1,5 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. use crate::{Color, Error, Viewport}; use raw_window_handle::HasRawWindowHandle; @@ -38,9 +40,13 @@ pub trait Compositor: Sized { height: u32, ); + /// Returns [`GraphicsInformation`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information; + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. /// - /// [`SwapChain`]: Self::SwapChain + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, @@ -51,7 +57,7 @@ pub trait Compositor: Sized { ) -> Result<(), SurfaceError>; } -/// Result of an unsuccessful call to [`Compositor::draw`]. +/// Result of an unsuccessful call to [`Compositor::present`]. #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { /// A timeout was encountered while trying to acquire the next frame. @@ -71,3 +77,12 @@ pub enum SurfaceError { #[error("There is no more memory left to allocate a new frame")] OutOfMemory, } + +/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). +#[derive(Debug)] +pub struct Information { + /// Contains the graphics adapter. + pub adapter: String, + /// Contains the graphics backend. + pub backend: String, +} diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index b1b995f1..722e4d9c 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -1,3 +1,6 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. +use crate::compositor::Information; use crate::{Color, Error, Size, Viewport}; use core::ffi::c_void; @@ -32,6 +35,9 @@ pub trait GLCompositor: Sized { /// Creates a new [`GLCompositor`] and [`Renderer`] with the given /// [`Settings`] and an OpenGL address loader function. /// + /// # Safety + /// The `loader_function` should resolve to valid OpenGL bindings. + /// /// [`Renderer`]: crate::Renderer /// [`Backend`]: crate::Backend /// [`Settings`]: Self::Settings @@ -48,6 +54,9 @@ pub trait GLCompositor: Sized { /// Resizes the viewport of the [`GLCompositor`]. fn resize_viewport(&mut self, physical_size: Size<u32>); + /// Returns [`GraphicsInformation`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information; + /// Presents the primitives of the [`Renderer`] to the next frame of the /// [`GLCompositor`]. /// diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index b840de50..12e4e313 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -1,11 +1,18 @@ [package] name = "iced_lazy" -version = "0.1.0" +version = "0.1.1" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" +description = "Lazy widgets for Iced" +license = "MIT" +repository = "https://github.com/iced-rs/iced" +documentation = "https://docs.rs/iced_lazy" +keywords = ["gui", "ui", "graphics", "interface", "widgets"] +categories = ["gui"] [dependencies] ouroboros = "0.13" [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" diff --git a/lazy/src/cache.rs b/lazy/src/cache.rs index 229b7912..5b4a39f6 100644 --- a/lazy/src/cache.rs +++ b/lazy/src/cache.rs @@ -9,5 +9,5 @@ pub struct Cache<'a, Message: 'a, Renderer: 'a> { #[borrows(mut element)] #[covariant] - pub overlay: Option<overlay::Element<'this, Message, Renderer>>, + overlay: Option<overlay::Element<'this, Message, Renderer>>, } diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 9e5937e9..8987b993 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -1,17 +1,17 @@ //! Build and reuse custom widgets using The Elm Architecture. -use crate::{Cache, CacheBuilder}; - use iced_native::event; use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::overlay; use iced_native::renderer; +use iced_native::widget; +use iced_native::widget::tree::{self, Tree}; use iced_native::{ Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; use ouroboros::self_referencing; -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::marker::PhantomData; /// A reusable, custom widget that uses The Elm Architecture. @@ -28,17 +28,24 @@ use std::marker::PhantomData; /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. pub trait Component<Message, Renderer> { + /// The internal state of this [`Component`]. + type State: Default; + /// The type of event this [`Component`] handles internally. type Event; /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. /// /// It can produce a `Message` for the parent application. - fn update(&mut self, event: Self::Event) -> Option<Message>; + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + ) -> Option<Message>; /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) /// on user interaction. - fn view(&mut self) -> Element<Self::Event, Renderer>; + fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; } /// Turns an implementor of [`Component`] into an [`Element`] that can be @@ -48,6 +55,7 @@ pub fn view<'a, C, Message, Renderer>( ) -> Element<'a, Message, Renderer> where C: Component<Message, Renderer> + 'a, + C::State: 'static, Message: 'a, Renderer: iced_native::Renderer + 'a, { @@ -56,36 +64,48 @@ where StateBuilder { component: Box::new(component), message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |_| None, - } - .build(), - ) - }, + state: PhantomData, + element_builder: |_| None, } .build(), )), }) } -struct Instance<'a, Message, Renderer, Event> { - state: RefCell<Option<State<'a, Message, Renderer, Event>>>, +struct Instance<'a, Message, Renderer, Event, S> { + state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, } #[self_referencing] -struct State<'a, Message: 'a, Renderer: 'a, Event: 'a> { - component: Box<dyn Component<Message, Renderer, Event = Event> + 'a>, +struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { + component: + Box<dyn Component<Message, Renderer, Event = Event, State = S> + 'a>, message: PhantomData<Message>, + state: PhantomData<S>, - #[borrows(mut component)] + #[borrows(component)] #[covariant] - cache: Option<Cache<'this, Event, Renderer>>, + element: Option<Element<'this, Event, Renderer>>, } -impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { +impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> +where + S: Default, +{ + fn rebuild_element(&self, state: &S) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| Some(component.view(state)), + } + .build(), + ); + } + fn with_element<T>( &self, f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, @@ -101,34 +121,43 @@ impl<'a, Message, Renderer, Event> Instance<'a, Message, Renderer, Event> { .borrow_mut() .as_mut() .unwrap() - .with_cache_mut(|cache| { - let mut element = cache.take().unwrap().into_heads().element; - let result = f(&mut element); - - *cache = Some( - CacheBuilder { - element, - overlay_builder: |_| None, - } - .build(), - ); - - result - }) + .with_element_mut(|element| f(element.as_mut().unwrap())) } } -impl<'a, Message, Renderer, Event> Widget<Message, Renderer> - for Instance<'a, Message, Renderer, Event> +impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer> + for Instance<'a, Message, Renderer, Event, S> where + S: 'static + Default, Renderer: iced_native::Renderer, { + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<S>>() + } + + fn state(&self) -> tree::State { + tree::State::new(S::default()) + } + + fn children(&self) -> Vec<Tree> { + self.rebuild_element(&S::default()); + self.with_element(|element| vec![Tree::new(element)]) + } + + fn diff(&self, tree: &mut Tree) { + self.rebuild_element(tree.state.downcast_ref()); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }) + } + fn width(&self) -> Length { - self.with_element(|element| element.width()) + self.with_element(|element| element.as_widget().width()) } fn height(&self) -> Length { - self.with_element(|element| element.height()) + self.with_element(|element| element.as_widget().height()) } fn layout( @@ -136,11 +165,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.with_element(|element| element.layout(renderer, limits)) + self.with_element(|element| { + element.as_widget().layout(renderer, limits) + }) } fn on_event( &mut self, + tree: &mut Tree, event: iced_native::Event, layout: Layout<'_>, cursor_position: Point, @@ -152,7 +184,8 @@ where let mut local_shell = Shell::new(&mut local_messages); let event_status = self.with_element_mut(|element| { - element.on_event( + element.as_widget_mut().on_event( + &mut tree.children[0], event, layout, cursor_position, @@ -165,37 +198,31 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { - let mut component = self - .state - .borrow_mut() - .take() - .unwrap() - .into_heads() - .component; - - for message in local_messages - .into_iter() - .filter_map(|message| component.update(message)) - { + let mut heads = self.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(tree.state.downcast_mut::<S>(), message) + }) { shell.publish(message); } - *self.state.borrow_mut() = Some( + self.state = RefCell::new(Some( StateBuilder { - component, + component: heads.component, message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |_| None, - } - .build(), - ) + state: PhantomData, + element_builder: |state| { + Some(state.view(tree.state.downcast_ref::<S>())) }, } .build(), - ); + )); + + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element)) + }); shell.invalidate_layout(); } @@ -203,28 +230,81 @@ where event_status } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<Message>, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation<B>, + } + + impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<T>, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + } + + self.with_element(|element| { + element.as_widget().operate( + &mut tree.children[0], + layout, + &mut MapOperation { operation }, + ); + }); + } + fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { self.with_element(|element| { - element.draw(renderer, style, layout, cursor_position, viewport); + element.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); }); } fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.with_element(|element| { - element.mouse_interaction( + element.as_widget().mouse_interaction( + &tree.children[0], layout, cursor_position, viewport, @@ -233,63 +313,72 @@ where }) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - let has_overlay = self - .state - .borrow_mut() - .as_mut() - .unwrap() - .with_cache_mut(|cache| { - let element = cache.take().unwrap().into_heads().element; - - *cache = Some( - CacheBuilder { - element, - overlay_builder: |element| { - element.overlay(layout, renderer) - }, - } - .build(), - ); - - cache + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let overlay = OverlayBuilder { + instance: self, + instance_ref_builder: |instance| instance.state.borrow(), + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance .as_ref() .unwrap() - .borrow_overlay() + .borrow_element() .as_ref() - .map(|overlay| overlay.position()) - }); + .unwrap() + .as_widget() + .overlay(&mut tree.children[0], layout, renderer) + }, + } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); has_overlay.map(|position| { overlay::Element::new( position, - Box::new(Overlay { instance: self }), + Box::new(OverlayInstance { + overlay: Some(overlay), + }), ) }) } } -struct Overlay<'a, 'b, Message, Renderer, Event> { - instance: &'b mut Instance<'a, Message, Renderer, Event>, +#[self_referencing] +struct Overlay<'a, 'b, Message, Renderer, Event, S> { + instance: &'a Instance<'b, Message, Renderer, Event, S>, + tree: &'a mut Tree, + types: PhantomData<(Message, Event, S)>, + + #[borrows(instance)] + #[covariant] + instance_ref: Ref<'this, Option<State<'a, Message, Renderer, Event, S>>>, + + #[borrows(instance_ref, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'this, Event, Renderer>>, +} + +struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { + overlay: Option<Overlay<'a, 'b, Message, Renderer, Event, S>>, } -impl<'a, 'b, Message, Renderer, Event> - Overlay<'a, 'b, Message, Renderer, Event> +impl<'a, 'b, Message, Renderer, Event, S> + OverlayInstance<'a, 'b, Message, Renderer, Event, S> { fn with_overlay_maybe<T>( &self, f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, ) -> Option<T> { - self.instance - .state - .borrow() - .as_ref() - .unwrap() - .borrow_cache() + self.overlay .as_ref() .unwrap() .borrow_overlay() @@ -298,27 +387,21 @@ impl<'a, 'b, Message, Renderer, Event> } fn with_overlay_mut_maybe<T>( - &self, + &mut self, f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, ) -> Option<T> { - self.instance - .state - .borrow_mut() + self.overlay .as_mut() .unwrap() - .with_cache_mut(|cache| { - cache - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - }) + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } -impl<'a, 'b, Message, Renderer, Event> overlay::Overlay<Message, Renderer> - for Overlay<'a, 'b, Message, Renderer, Event> +impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer> + for OverlayInstance<'a, 'b, Message, Renderer, Event, S> where Renderer: iced_native::Renderer, + S: 'static + Default, { fn layout( &self, @@ -337,12 +420,13 @@ where fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ) { - self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, style, layout, cursor_position); + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); }); } @@ -387,39 +471,48 @@ where &mut local_shell, ) }) - .unwrap_or_else(|| iced_native::event::Status::Ignored); + .unwrap_or(iced_native::event::Status::Ignored); local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { - let mut component = - self.instance.state.take().unwrap().into_heads().component; - - for message in local_messages - .into_iter() - .filter_map(|message| component.update(message)) - { + let overlay = self.overlay.take().unwrap().into_heads(); + let mut heads = overlay.instance.state.take().unwrap().into_heads(); + + for message in local_messages.into_iter().filter_map(|message| { + heads + .component + .update(overlay.tree.state.downcast_mut::<S>(), message) + }) { shell.publish(message); } - self.instance.state = RefCell::new(Some( + *overlay.instance.state.borrow_mut() = Some( StateBuilder { - component, + component: heads.component, message: PhantomData, - cache_builder: |state| { - Some( - CacheBuilder { - element: state.view(), - overlay_builder: |element| { - element.overlay(layout, renderer) - }, - } - .build(), - ) + state: PhantomData, + element_builder: |state| { + Some(state.view(overlay.tree.state.downcast_ref::<S>())) }, } .build(), - )); + ); + + overlay.instance.with_element(|element| { + overlay.tree.diff_children(std::slice::from_ref(&element)) + }); + + self.overlay = Some( + OverlayBuilder { + instance: overlay.instance, + instance_ref_builder: |instance| instance.state.borrow(), + tree: overlay.tree, + types: PhantomData, + overlay_builder: |_, _| None, + } + .build(), + ); shell.invalidate_layout(); } diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index 05fce765..3827746c 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -1,3 +1,22 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![deny( + missing_debug_implementations, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code)] +#![allow( + clippy::await_holding_refcell_ref, + clippy::inherent_to_string, + clippy::type_complexity +)] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod component; pub mod responsive; @@ -6,4 +25,27 @@ pub use responsive::Responsive; mod cache; -use cache::{Cache, CacheBuilder}; +use iced_native::{Element, Size}; + +/// Turns an implementor of [`Component`] into an [`Element`] that can be +/// embedded in any application. +pub fn component<'a, C, Message, Renderer>( + component: C, +) -> Element<'a, Message, Renderer> +where + C: Component<Message, Renderer> + 'a, + C::State: 'static, + Message: 'a, + Renderer: iced_native::Renderer + 'a, +{ + component::view(component) +} + +pub fn responsive<'a, Message, Renderer>( + f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, +) -> Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + Responsive::new(f) +} diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 20a80dac..0b7ae6de 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -1,71 +1,131 @@ -//! Build responsive widgets. -use crate::{Cache, CacheBuilder}; - -use iced_native::event::{self, Event}; +use iced_native::event; use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::overlay; use iced_native::renderer; -use iced_native::window; +use iced_native::widget::horizontal_space; +use iced_native::widget::tree::{self, Tree}; use iced_native::{ Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; -use std::cell::RefCell; +use ouroboros::self_referencing; +use std::cell::{RefCell, RefMut}; +use std::marker::PhantomData; use std::ops::Deref; -/// The state of a [`Responsive`] widget. -#[derive(Debug, Clone, Default)] -pub struct State { - last_size: Option<Size>, - last_layout: layout::Node, -} - -impl State { - pub fn new() -> State { - State::default() - } - - fn layout(&self, parent: Layout<'_>) -> Layout<'_> { - Layout::with_offset( - parent.position() - Point::ORIGIN, - &self.last_layout, - ) - } -} - /// A widget that is aware of its dimensions. /// /// A [`Responsive`] widget will always try to fill all the available space of /// its parent. #[allow(missing_debug_implementations)] -pub struct Responsive<'a, Message, Renderer>( - RefCell<Internal<'a, Message, Renderer>>, -); +pub struct Responsive<'a, Message, Renderer> { + view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>, + content: RefCell<Content<'a, Message, Renderer>>, +} -impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> { - /// Creates a new [`Responsive`] widget with the given [`State`] and a - /// closure that produces its contents. +impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + /// Creates a new [`Responsive`] widget with a closure that produces its + /// contents. /// /// 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. pub fn new( - state: &'a mut State, - view: impl FnOnce(Size) -> Element<'a, Message, Renderer> + 'a, + view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, ) -> Self { - Self(RefCell::new(Internal { - state, - content: Content::Pending(Some(Box::new(view))), - })) + Self { + view: Box::new(view), + content: RefCell::new(Content { + size: Size::ZERO, + layout: layout::Node::new(Size::ZERO), + element: Element::new(horizontal_space(Length::Units(0))), + }), + } + } +} + +struct Content<'a, Message, Renderer> { + size: Size, + layout: layout::Node, + element: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> Content<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, +{ + fn update( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + new_size: Size, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + ) { + if self.size == new_size { + return; + } + + self.element = view(new_size); + self.size = new_size; + + tree.diff(&self.element); + + self.layout = self + .element + .as_widget() + .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); + } + + fn resolve<R, T>( + &mut self, + tree: &mut Tree, + renderer: R, + layout: Layout<'_>, + view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, + f: impl FnOnce( + &mut Tree, + R, + Layout<'_>, + &mut Element<'a, Message, Renderer>, + ) -> T, + ) -> T + where + R: Deref<Target = Renderer>, + { + self.update(tree, renderer.deref(), layout.bounds().size(), view); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &self.layout, + ); + + f(tree, renderer, content_layout, &mut self.element) } } +struct State { + tree: RefCell<Tree>, +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Responsive<'a, Message, Renderer> where Renderer: iced_native::Renderer, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + tree: RefCell::new(Tree::empty()), + }) + } + fn width(&self) -> Length { Length::Fill } @@ -79,212 +139,141 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.max(); - - self.0.borrow_mut().state.last_size = Some(size); - - layout::Node::new(size) + layout::Node::new(limits.max()) } fn on_event( &mut self, - event: Event, + tree: &mut Tree, + event: iced_native::Event, layout: Layout<'_>, cursor_position: Point, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let mut internal = self.0.borrow_mut(); - - if matches!(event, Event::Window(window::Event::Resized { .. })) - || internal.state.last_size - != Some(internal.state.last_layout.size()) - { - shell.invalidate_widgets(); - } - - internal.resolve(renderer, |state, renderer, content| { - content.on_event( - event, - state.layout(layout), - cursor_position, - renderer, - clipboard, - shell, - ) - }) + let state = tree.state.downcast_mut::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget_mut().on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - let mut internal = self.0.borrow_mut(); - - internal.resolve(renderer, |state, renderer, content| { - content.draw( - renderer, - style, - state.layout(layout), - cursor_position, - viewport, - ) - }) + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) } fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let mut internal = self.0.borrow_mut(); - - internal.resolve(renderer, |state, renderer, content| { - content.mouse_interaction( - state.layout(layout), - cursor_position, - viewport, - renderer, - ) - }) + let state = tree.state.downcast_ref::<State>(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - let has_overlay = { - use std::ops::DerefMut; - - let mut internal = self.0.borrow_mut(); - - let _ = - internal.resolve(renderer, |_state, _renderer, _content| {}); - - let Internal { content, state } = internal.deref_mut(); - - let content_layout = state.layout(layout); - - match content { - Content::Pending(_) => None, - Content::Ready(cache) => { - *cache = Some( - CacheBuilder { - element: cache.take().unwrap().into_heads().element, - overlay_builder: |element| { - element.overlay(content_layout, renderer) - }, - } - .build(), - ); - - cache - .as_ref() - .unwrap() - .borrow_overlay() - .as_ref() - .map(|overlay| overlay.position()) - } - } - }; - - has_overlay.map(|position| { - overlay::Element::new( - position, - Box::new(Overlay { instance: self }), - ) - }) - } -} - -struct Internal<'a, Message, Renderer> { - state: &'a mut State, - content: Content<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Internal<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn resolve<R, T>( - &mut self, - renderer: R, - f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, - ) -> T - where - R: Deref<Target = Renderer>, - { - self.content.resolve(&mut self.state, renderer, f) - } -} - -enum Content<'a, Message, Renderer> { - Pending( - Option<Box<dyn FnOnce(Size) -> Element<'a, Message, Renderer> + 'a>>, - ), - Ready(Option<Cache<'a, Message, Renderer>>), -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn resolve<R, T>( - &mut self, - state: &mut State, - renderer: R, - f: impl FnOnce(&State, R, &mut Element<'a, Message, Renderer>) -> T, - ) -> T - where - R: Deref<Target = Renderer>, - { - match self { - Content::Ready(cache) => { - let mut heads = cache.take().unwrap().into_heads(); - - let result = f(state, renderer, &mut heads.element); - - *cache = Some( - CacheBuilder { - element: heads.element, - overlay_builder: |_| None, - } - .build(), + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_ref::<State>(); + + let overlay = OverlayBuilder { + content: self.content.borrow_mut(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content, tree| { + content.update( + tree, + renderer, + layout.bounds().size(), + &self.view, ); - result - } - Content::Pending(view) => { - let element = - view.take().unwrap()(state.last_size.unwrap_or(Size::ZERO)); - - state.last_layout = element.layout( - renderer.deref(), - &layout::Limits::new( - Size::ZERO, - state.last_size.unwrap_or(Size::ZERO), - ), + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + &content.layout, ); - *self = Content::Ready(Some( - CacheBuilder { - element, - overlay_builder: |_| None, - } - .build(), - )); - - self.resolve(state, renderer, f) - } + content.element.as_widget().overlay( + tree, + content_layout, + renderer, + ) + }, } + .build(); + + let has_overlay = overlay.with_overlay(|overlay| { + overlay.as_ref().map(overlay::Element::position) + }); + + has_overlay + .map(|position| overlay::Element::new(position, Box::new(overlay))) } } @@ -299,8 +288,15 @@ where } } +#[self_referencing] struct Overlay<'a, 'b, Message, Renderer> { - instance: &'b mut Responsive<'a, Message, Renderer>, + content: RefMut<'a, Content<'b, Message, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData<Message>, + + #[borrows(mut content, mut tree)] + #[covariant] + overlay: Option<overlay::Element<'this, Message, Renderer>>, } impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { @@ -308,29 +304,14 @@ impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { &self, f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, ) -> Option<T> { - let internal = self.instance.0.borrow(); - - match &internal.content { - Content::Pending(_) => None, - Content::Ready(cache) => { - cache.as_ref().unwrap().borrow_overlay().as_ref().map(f) - } - } + self.borrow_overlay().as_ref().map(f) } fn with_overlay_mut_maybe<T>( - &self, + &mut self, f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, ) -> Option<T> { - let mut internal = self.instance.0.borrow_mut(); - - match &mut internal.content { - Content::Pending(_) => None, - Content::Ready(cache) => cache - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)), - } + self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } @@ -356,12 +337,13 @@ where fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ) { - self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, style, layout, cursor_position); + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor_position); }); } @@ -391,7 +373,7 @@ where renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> iced_native::event::Status { + ) -> event::Status { self.with_overlay_mut_maybe(|overlay| { overlay.on_event( event, @@ -402,6 +384,6 @@ where shell, ) }) - .unwrap_or_else(|| iced_native::event::Status::Ignored) + .unwrap_or(iced_native::event::Status::Ignored) } } diff --git a/native/Cargo.toml b/native/Cargo.toml index c4b363ae..b4945c05 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.4.0" +version = "0.5.1" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A renderer-agnostic library for native GUIs" @@ -16,14 +16,14 @@ unicode-segmentation = "1.6" num-traits = "0.2" [dependencies.iced_core] -version = "0.4" +version = "0.5" path = "../core" [dependencies.iced_futures] -version = "0.3" +version = "0.4" path = "../futures" features = ["thread-pool"] [dependencies.iced_style] -version = "0.3" +version = "0.4" path = "../style" diff --git a/native/src/command.rs b/native/src/command.rs index 89d0f045..89ee7375 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -3,6 +3,8 @@ mod action; pub use action::Action; +use crate::widget; + use iced_futures::MaybeSend; use std::fmt; @@ -24,10 +26,17 @@ impl<T> Command<T> { Self(iced_futures::Command::single(action)) } + /// Creates a [`Command`] that performs a [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self { + Self(iced_futures::Command::single(Action::Widget( + widget::Action::new(operation), + ))) + } + /// Creates a [`Command`] that performs the action of the given future. pub fn perform<A>( future: impl Future<Output = T> + 'static + MaybeSend, - f: impl Fn(T) -> A + 'static + MaybeSend, + f: impl FnOnce(T) -> A + 'static + MaybeSend, ) -> Command<A> { use iced_futures::futures::FutureExt; @@ -51,6 +60,7 @@ impl<T> Command<T> { ) -> Command<A> where T: 'static, + A: 'static, { let Command(command) = self; diff --git a/native/src/command/action.rs b/native/src/command/action.rs index 5c7509c8..a6954f8f 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,4 +1,6 @@ use crate::clipboard; +use crate::system; +use crate::widget; use crate::window; use iced_futures::MaybeSend; @@ -10,22 +12,33 @@ use std::fmt; /// [`Command`]: crate::Command pub enum Action<T> { /// Run a [`Future`] to completion. + /// + /// [`Future`]: iced_futures::BoxFuture Future(iced_futures::BoxFuture<T>), /// Run a clipboard action. Clipboard(clipboard::Action<T>), /// Run a window action. - Window(window::Action), + Window(window::Action<T>), + + /// Run a system action. + System(system::Action<T>), + + /// Run a widget action. + Widget(widget::Action<T>), } impl<T> Action<T> { /// Applies a transformation to the result of a [`Command`]. + /// + /// [`Command`]: crate::Command pub fn map<A>( self, f: impl Fn(T) -> A + 'static + MaybeSend + Sync, ) -> Action<A> where + A: 'static, T: 'static, { use iced_futures::futures::FutureExt; @@ -33,7 +46,9 @@ impl<T> Action<T> { match self { Self::Future(future) => Action::Future(Box::pin(future.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window), + Self::Window(window) => Action::Window(window.map(f)), + Self::System(system) => Action::System(system.map(f)), + Self::Widget(widget) => Action::Widget(widget.map(f)), } } } @@ -46,6 +61,8 @@ impl<T> fmt::Debug for Action<T> { write!(f, "Action::Clipboard({:?})", action) } Self::Window(action) => write!(f, "Action::Window({:?})", action), + Self::System(action) => write!(f, "Action::System({:?})", action), + Self::Widget(_action) => write!(f, "Action::Widget"), } } } diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs index d706bb00..603f2fd5 100644 --- a/native/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -186,6 +186,12 @@ impl Debug { } } +impl Default for Debug { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug)] struct TimeBuffer { head: usize, diff --git a/native/src/debug/null.rs b/native/src/debug/null.rs index 60e6122d..2db0eebb 100644 --- a/native/src/debug/null.rs +++ b/native/src/debug/null.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Debug; impl Debug { diff --git a/native/src/element.rs b/native/src/element.rs index 119b7892..074e422e 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -3,10 +3,14 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, }; +use std::borrow::Borrow; + /// A generic [`Widget`]. /// /// It is useful to build composable user interfaces that do not leak @@ -15,25 +19,33 @@ use crate::{ /// If you have a [built-in widget], you should be able to use `Into<Element>` /// to turn it into an [`Element`]. /// -/// [built-in widget]: widget/index.html#built-in-widgets +/// [built-in widget]: crate::widget #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { - pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>, + widget: Box<dyn Widget<Message, Renderer> + 'a>, } -impl<'a, Message, Renderer> Element<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new( - widget: impl Widget<Message, Renderer> + 'a, - ) -> Element<'a, Message, Renderer> { - Element { + pub fn new(widget: impl Widget<Message, Renderer> + 'a) -> Self + where + Renderer: crate::Renderer, + { + Self { widget: Box::new(widget), } } + /// Returns a reference to the [`Widget`] of the [`Element`], + pub fn as_widget(&self) -> &dyn Widget<Message, Renderer> { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget<Message, Renderer> { + self.widget.as_mut() + } + /// Applies a transformation to the produced message of the [`Element`]. /// /// This method is useful when you want to decouple different parts of your @@ -79,7 +91,7 @@ where /// /// ``` /// # mod counter { - /// # type Text = iced_native::widget::Text<iced_native::renderer::Null>; + /// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>; /// # /// # #[derive(Debug, Clone, Copy)] /// # pub enum Message {} @@ -168,16 +180,16 @@ where /// } /// } /// ``` - pub fn map<F, B>(self, f: F) -> Element<'a, B, Renderer> + pub fn map<B>( + self, + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> where - Message: 'static, - Renderer: 'a, - B: 'static, - F: 'static + Fn(Message) -> B, + Message: 'a, + Renderer: crate::Renderer + 'a, + B: 'a, { - Element { - widget: Box::new(Map::new(self.widget, f)), - } + Element::new(Map::new(self.widget, f)) } /// Marks the [`Element`] as _to-be-explained_. @@ -192,96 +204,33 @@ where ) -> Element<'a, Message, Renderer> where Message: 'static, - Renderer: 'a, + Renderer: crate::Renderer + 'a, { Element { widget: Box::new(Explain::new(self, color.into())), } } +} - /// Returns the width of the [`Element`]. - pub fn width(&self) -> Length { - self.widget.width() - } - - /// Returns the height of the [`Element`]. - pub fn height(&self) -> Length { - self.widget.height() - } - - /// Computes the layout of the [`Element`] in the given [`Limits`]. - /// - /// [`Limits`]: layout::Limits - pub fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - /// Processes a runtime [`Event`]. - pub fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.widget.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget - .draw(renderer, style, layout, cursor_position, viewport) - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.widget.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) +impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) { + self.widget.borrow() } +} - /// Returns the overlay of the [`Element`], if there is any. - pub fn overlay<'b>( - &'b mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'b, Message, Renderer>> { - self.widget.overlay(layout, renderer) +impl<'a, Message, Renderer> Borrow<dyn Widget<Message, Renderer> + 'a> + for &Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget<Message, Renderer> + 'a) { + self.widget.borrow() } } struct Map<'a, A, B, Renderer> { widget: Box<dyn Widget<A, Renderer> + 'a>, - mapper: Box<dyn Fn(A) -> B>, + mapper: Box<dyn Fn(A) -> B + 'a>, } impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { @@ -290,7 +239,7 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { mapper: F, ) -> Map<'a, A, B, Renderer> where - F: 'static + Fn(A) -> B, + F: 'a + Fn(A) -> B, { Map { widget, @@ -302,9 +251,25 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer> where Renderer: crate::Renderer + 'a, - A: 'static, - B: 'static, + A: 'a, + B: 'a, { + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec<Tree> { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + fn width(&self) -> Length { self.widget.width() } @@ -321,8 +286,45 @@ where self.widget.layout(renderer, limits) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<B>, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation<B>, + } + + impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<T>, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + } + + self.widget + .operate(tree, layout, &mut MapOperation { operation }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -334,6 +336,7 @@ where let mut local_shell = Shell::new(&mut local_messages); let status = self.widget.on_event( + tree, event, layout, cursor_position, @@ -349,24 +352,35 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - self.widget - .draw(renderer, style, layout, cursor_position, viewport) + self.widget.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) } fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.widget.mouse_interaction( + tree, layout, cursor_position, viewport, @@ -374,15 +388,16 @@ where ) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, B, Renderer>> { + ) -> Option<overlay::Element<'b, B, Renderer>> { let mapper = &self.mapper; self.widget - .overlay(layout, renderer) + .overlay(tree, layout, renderer) .map(move |overlay| overlay.map(mapper)) } } @@ -414,6 +429,22 @@ where self.element.widget.height() } + fn tag(&self) -> tree::Tag { + self.element.widget.tag() + } + + fn state(&self) -> tree::State { + self.element.widget.state() + } + + fn children(&self) -> Vec<Tree> { + self.element.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.element.widget.diff(tree); + } + fn layout( &self, renderer: &Renderer, @@ -422,8 +453,18 @@ where self.element.widget.layout(renderer, limits) } + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<Message>, + ) { + self.element.widget.operate(state, layout, operation) + } + fn on_event( &mut self, + state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -432,6 +473,7 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { self.element.widget.on_event( + state, event, layout, cursor_position, @@ -443,7 +485,9 @@ where fn draw( &self, + state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -470,7 +514,9 @@ where } self.element.widget.draw( + state, renderer, + theme, style, layout, cursor_position, @@ -482,12 +528,14 @@ where fn mouse_interaction( &self, + state: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.element.widget.mouse_interaction( + state, layout, cursor_position, viewport, @@ -495,11 +543,12 @@ where ) } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.element.overlay(layout, renderer) + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.element.widget.overlay(state, layout, renderer) } } diff --git a/native/src/event.rs b/native/src/event.rs index 27540a25..bcfaf891 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -29,14 +29,14 @@ pub enum Event { } /// A platform specific event -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum PlatformSpecific { /// A MacOS specific event MacOS(MacOS), } /// Describes an event specific to MacOS -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum MacOS { /// Triggered when the app receives an URL from the system /// diff --git a/native/src/hasher.rs b/native/src/hasher.rs index 9f6aacce..fa52f16d 100644 --- a/native/src/hasher.rs +++ b/native/src/hasher.rs @@ -1,13 +1,7 @@ /// The hasher used to compare layouts. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Hasher(twox_hash::XxHash64); -impl Default for Hasher { - fn default() -> Self { - Hasher(twox_hash::XxHash64::default()) - } -} - impl core::hash::Hasher for Hasher { fn write(&mut self, bytes: &[u8]) { self.0.write(bytes) diff --git a/native/src/image.rs b/native/src/image.rs index 43bba4f1..516eb2db 100644 --- a/native/src/image.rs +++ b/native/src/image.rs @@ -5,7 +5,7 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; -/// An [`Image`] handle. +/// A handle of some image data. #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -79,7 +79,7 @@ impl Hash for Handle { } } -/// The data of an [`Image`]. +/// The data of a raster image. #[derive(Clone, Hash)] pub enum Data { /// File data diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs index 5fbcbca0..94121d76 100644 --- a/native/src/layout/flex.rs +++ b/native/src/layout/flex.rs @@ -16,8 +16,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use crate::Element; + use crate::layout::{Limits, Node}; -use crate::{Alignment, Element, Padding, Point, Size}; +use crate::{Alignment, Padding, Point, Size}; /// The main axis of a flex layout. #[derive(Debug)] @@ -84,8 +86,8 @@ where items.iter().for_each(|child| { let cross_fill_factor = match axis { - Axis::Horizontal => child.height(), - Axis::Vertical => child.width(), + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), } .fill_factor(); @@ -95,7 +97,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -107,8 +109,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -130,7 +132,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -149,8 +151,8 @@ where for (i, child) in items.iter().enumerate() { let fill_factor = match axis { - Axis::Horizontal => child.width(), - Axis::Vertical => child.height(), + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), } .fill_factor(); @@ -179,7 +181,7 @@ where Size::new(max_width, max_height), ); - let layout = child.layout(renderer, &child_limits); + let layout = child.as_widget().layout(renderer, &child_limits); if align_items != Alignment::Fill { cross = cross.max(axis.cross(layout.size())); diff --git a/native/src/lib.rs b/native/src/lib.rs index 5c9c24c9..13173901 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -23,19 +23,27 @@ //! - Build a new renderer, see the [renderer] module. //! - Build a custom widget, start at the [`Widget`] trait. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/master/core -//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/master/winit +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.4/core +//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.4/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [renderer]: crate::renderer #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![forbid(unsafe_code)] -#![forbid(rust_2018_idioms)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; pub mod command; pub mod event; @@ -48,6 +56,7 @@ pub mod program; pub mod renderer; pub mod subscription; pub mod svg; +pub mod system; pub mod text; pub mod touch; pub mod user_interface; @@ -75,6 +84,8 @@ pub use iced_core::{ Rectangle, Size, Vector, }; pub use iced_futures::{executor, futures}; +pub use iced_style::application; +pub use iced_style::theme; #[doc(no_inline)] pub use executor::Executor; @@ -92,5 +103,6 @@ pub use renderer::Renderer; pub use runtime::Runtime; pub use shell::Shell; pub use subscription::Subscription; +pub use theme::Theme; pub use user_interface::UserInterface; pub use widget::Widget; diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 124bcac2..905d3389 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -10,6 +10,8 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; /// An interactive component that can be displayed on top of other widgets. @@ -34,11 +36,42 @@ where fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ); + /// Returns the [`Tag`] of the [`Widget`]. + /// + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec<Tree> { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _layout: Layout<'_>, + _operation: &mut dyn widget::Operation<Message>, + ) { + } + /// Processes a runtime [`Event`]. /// /// It receives: @@ -63,7 +96,7 @@ where event::Status::Ignored } - /// Returns the current [`mouse::Interaction`] of the [`Widget`]. + /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. /// /// By default, it returns [`mouse::Interaction::Idle`]. fn mouse_interaction( @@ -76,3 +109,26 @@ where mouse::Interaction::Idle } } + +/// Obtains the first overlay [`Element`] found in the given children. +/// +/// This method will generally only be used by advanced users that are +/// implementing the [`Widget`](crate::Widget) trait. +pub fn from_children<'a, Message, Renderer>( + children: &'a [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option<Element<'a, Message, Renderer>> +where + Renderer: crate::Renderer, +{ + children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget().overlay(state, layout, renderer) + }) + .next() +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index b60881e3..b919c221 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -4,6 +4,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; /// A generic [`Overlay`]. @@ -41,7 +42,7 @@ where where Message: 'a, Renderer: 'a, - B: 'static, + B: 'a, { Element { position: self.position, @@ -94,11 +95,22 @@ where pub fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ) { - self.overlay.draw(renderer, style, layout, cursor_position) + self.overlay + .draw(renderer, theme, style, layout, cursor_position) + } + + /// Applies an [`Operation`] to the [`Element`]. + pub fn operate( + &self, + layout: Layout<'_>, + operation: &mut dyn widget::Operation<Message>, + ) { + self.overlay.operate(layout, operation); } } @@ -173,10 +185,12 @@ where fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ) { - self.content.draw(renderer, style, layout, cursor_position) + self.content + .draw(renderer, theme, style, layout, cursor_position) } } diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 13fa7beb..08135872 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -7,18 +7,23 @@ use crate::overlay; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget::container::{self, Container}; use crate::widget::scrollable::{self, Scrollable}; -use crate::widget::Container; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_style::menu::Style; +pub use iced_style::menu::{Appearance, StyleSheet}; /// A list of selectable options. #[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer: text::Renderer> { +pub struct Menu<'a, T, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, @@ -27,13 +32,15 @@ pub struct Menu<'a, T, Renderer: text::Renderer> { padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style: Style, + style: <Renderer::Theme as StyleSheet>::Style, } impl<'a, T, Renderer> Menu<'a, T, Renderer> where T: ToString + Clone, Renderer: text::Renderer + 'a, + Renderer::Theme: + StyleSheet + container::StyleSheet + scrollable::StyleSheet, { /// Creates a new [`Menu`] with the given [`State`], a list of options, and /// the message to produced when an option is selected. @@ -81,7 +88,10 @@ where } /// Sets the style of the [`Menu`]. - pub fn style(mut self, style: impl Into<Style>) -> Self { + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { self.style = style.into(); self } @@ -105,29 +115,45 @@ where } /// The local state of a [`Menu`]. -#[derive(Debug, Clone, Default)] +#[derive(Debug)] pub struct State { - scrollable: scrollable::State, + tree: Tree, } impl State { /// Creates a new [`State`] for a [`Menu`]. pub fn new() -> Self { - Self::default() + Self { + tree: Tree::empty(), + } } } -struct Overlay<'a, Message, Renderer: text::Renderer> { +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +struct Overlay<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + state: &'a mut Tree, container: Container<'a, Message, Renderer>, width: u16, target_height: f32, - style: Style, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, Message, Renderer: text::Renderer> Overlay<'a, Message, Renderer> +impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> where Message: 'a, Renderer: 'a, + Renderer: text::Renderer, + Renderer::Theme: + StyleSheet + container::StyleSheet + scrollable::StyleSheet, { pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self where @@ -145,23 +171,24 @@ where style, } = menu; - let container = - Container::new(Scrollable::new(&mut state.scrollable).push(List { - options, - hovered_option, - last_selection, - font, - text_size, - padding, - style: style.clone(), - })) - .padding(1); + let container = Container::new(Scrollable::new(List { + options, + hovered_option, + last_selection, + font, + text_size, + padding, + style, + })); + + state.tree.diff(&container as &dyn Widget<_, _>); Self { + state: &mut state.tree, container, - width: width, + width, target_height, - style: style, + style, } } } @@ -170,7 +197,20 @@ impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> for Overlay<'a, Message, Renderer> where Renderer: text::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, { + fn tag(&self) -> tree::Tag { + self.container.tag() + } + + fn state(&self) -> tree::State { + self.container.state() + } + + fn children(&self) -> Vec<Tree> { + self.container.children() + } + fn layout( &self, renderer: &Renderer, @@ -214,7 +254,8 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { self.container.on_event( - event.clone(), + self.state, + event, layout, cursor_position, renderer, @@ -231,6 +272,7 @@ where renderer: &Renderer, ) -> mouse::Interaction { self.container.mouse_interaction( + self.state, layout, cursor_position, viewport, @@ -241,35 +283,51 @@ where fn draw( &self, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, ) { + let appearance = theme.appearance(self.style); let bounds = layout.bounds(); renderer.fill_quad( renderer::Quad { - bounds, - border_color: self.style.border_color, - border_width: self.style.border_width, - border_radius: 0.0, + bounds: Rectangle { + width: bounds.width - 1.0, + ..bounds + }, + border_color: appearance.border_color, + border_width: appearance.border_width, + border_radius: appearance.border_radius, }, - self.style.background, + appearance.background, ); - self.container - .draw(renderer, style, layout, cursor_position, &bounds); + self.container.draw( + self.state, + renderer, + theme, + style, + layout, + cursor_position, + &bounds, + ); } } -struct List<'a, T, Renderer: text::Renderer> { +struct List<'a, T, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ options: &'a [T], hovered_option: &'a mut Option<usize>, last_selection: &'a mut Option<T>, padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style: Style, + style: <Renderer::Theme as StyleSheet>::Style, } impl<'a, T, Message, Renderer> Widget<Message, Renderer> @@ -277,6 +335,7 @@ impl<'a, T, Message, Renderer> Widget<Message, Renderer> where T: Clone + ToString, Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { fn width(&self) -> Length { Length::Fill @@ -294,7 +353,8 @@ where use std::f32; let limits = limits.width(Length::Fill).height(Length::Shrink); - let text_size = self.text_size.unwrap_or(renderer.default_size()); + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); let size = { let intrinsic = Size::new( @@ -311,6 +371,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -334,8 +395,9 @@ where let bounds = layout.bounds(); if bounds.contains(cursor_position) { - let text_size = - self.text_size.unwrap_or(renderer.default_size()); + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); *self.hovered_option = Some( ((cursor_position.y - bounds.y) @@ -348,8 +410,9 @@ where let bounds = layout.bounds(); if bounds.contains(cursor_position) { - let text_size = - self.text_size.unwrap_or(renderer.default_size()); + let text_size = self + .text_size + .unwrap_or_else(|| renderer.default_size()); *self.hovered_option = Some( ((cursor_position.y - bounds.y) @@ -372,6 +435,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -388,15 +452,19 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, viewport: &Rectangle, ) { + let appearance = theme.appearance(self.style); let bounds = layout.bounds(); - let text_size = self.text_size.unwrap_or(renderer.default_size()); + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); let option_height = (text_size + self.padding.vertical()) as usize; let offset = viewport.y - bounds.y; @@ -423,9 +491,9 @@ where bounds, border_color: Color::TRANSPARENT, border_width: 0.0, - border_radius: 0.0, + border_radius: appearance.border_radius, }, - self.style.selected_background, + appearance.selected_background, ); } @@ -440,9 +508,9 @@ where size: f32::from(text_size), font: self.font.clone(), color: if is_selected { - self.style.selected_text_color + appearance.selected_text_color } else { - self.style.text_color + appearance.text_color }, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, @@ -451,14 +519,15 @@ where } } -impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> - for List<'a, T, Renderer> +impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>> + for Element<'a, Message, Renderer> where T: ToString + Clone, Message: 'a, Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) + fn from(list: List<'a, T, Renderer>) -> Self { + Element::new(list) } } diff --git a/native/src/program.rs b/native/src/program.rs index 9ee72703..c71c237f 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -26,5 +26,5 @@ pub trait Program: Sized { /// Returns the widgets to display in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>; + fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; } diff --git a/native/src/program/state.rs b/native/src/program/state.rs index cb87a628..2ddde2c2 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,4 +1,6 @@ +use crate::application; use crate::mouse; +use crate::renderer; use crate::user_interface::{self, UserInterface}; use crate::{Clipboard, Command, Debug, Event, Point, Program, Size}; @@ -19,6 +21,7 @@ where impl<P> State<P> where P: Program + 'static, + <P::Renderer as crate::Renderer>::Theme: application::StyleSheet, { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. @@ -86,6 +89,8 @@ where bounds: Size, cursor_position: Point, renderer: &mut P::Renderer, + theme: &<P::Renderer as crate::Renderer>::Theme, + style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, ) -> Option<Command<P::Message>> { @@ -108,14 +113,14 @@ where &mut messages, ); - messages.extend(self.queued_messages.drain(..)); + messages.append(&mut self.queued_messages); self.queued_events.clear(); debug.event_processing_finished(); if messages.is_empty() { debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, cursor_position); + user_interface.draw(renderer, theme, style, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -147,7 +152,7 @@ where debug.draw_started(); self.mouse_interaction = - user_interface.draw(renderer, cursor_position); + user_interface.draw(renderer, theme, style, cursor_position); debug.draw_finished(); self.cache = Some(user_interface.into_cache()); @@ -163,7 +168,10 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, debug: &mut Debug, -) -> UserInterface<'a, P::Message, P::Renderer> { +) -> UserInterface<'a, P::Message, P::Renderer> +where + <P::Renderer as crate::Renderer>::Theme: application::StyleSheet, +{ debug.view_started(); let view = program.view(); debug.view_finished(); diff --git a/native/src/renderer.rs b/native/src/renderer.rs index ca7ad5a2..ef64ac36 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -1,24 +1,4 @@ //! Write your own renderer. -//! -//! You will need to implement the `Renderer` trait first. It simply contains -//! an `Output` associated type. -//! -//! There is no common trait to draw all the widgets. Instead, every [`Widget`] -//! constrains its generic `Renderer` type as necessary. -//! -//! This approach is flexible and composable. For instance, the -//! [`Text`] widget only needs a [`text::Renderer`] while a [`Checkbox`] widget -//! needs both a [`text::Renderer`] and a [`checkbox::Renderer`], reusing logic. -//! -//! In the end, a __renderer__ satisfying all the constraints is -//! needed to build a [`UserInterface`]. -//! -//! [`Widget`]: crate::Widget -//! [`UserInterface`]: crate::UserInterface -//! [`Text`]: crate::widget::Text -//! [`text::Renderer`]: crate::widget::text::Renderer -//! [`Checkbox`]: crate::widget::Checkbox -//! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer #[cfg(debug_assertions)] mod null; #[cfg(debug_assertions)] @@ -27,9 +7,11 @@ pub use null::Null; use crate::layout; use crate::{Background, Color, Element, Rectangle, Vector}; -/// A component that can take the state of a user interface and produce an -/// output for its users. +/// A component that can be used by widgets to draw themselves on a screen. pub trait Renderer: Sized { + /// The supported theme of the [`Renderer`]. + type Theme; + /// Lays out the elements of a user interface. /// /// You should override this if you need to perform any operations before or @@ -39,7 +21,7 @@ pub trait Renderer: Sized { element: &Element<'a, Message, Self>, limits: &layout::Limits, ) -> layout::Node { - element.layout(self, limits) + element.as_widget().layout(self, limits) } /// Draws the primitives recorded in the given closure in a new layer. diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index a5b2f277..b1743dbf 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -1,11 +1,11 @@ use crate::renderer::{self, Renderer}; use crate::text::{self, Text}; -use crate::{Background, Font, Point, Rectangle, Size, Vector}; +use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector}; /// A renderer that does nothing. /// /// It can be useful if you are writing tests! -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct Null; impl Null { @@ -16,6 +16,8 @@ impl Null { } impl Renderer for Null { + type Theme = Theme; + fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} fn with_translation( diff --git a/native/src/shell.rs b/native/src/shell.rs index 4a0aa9c6..b96d23e5 100644 --- a/native/src/shell.rs +++ b/native/src/shell.rs @@ -31,6 +31,11 @@ impl<'a, Message> Shell<'a, Message> { } } + /// Returns whether the current layout is invalid or not. + pub fn is_layout_invalid(&self) -> bool { + self.is_layout_invalid + } + /// Publish the given `Message` for an application to process it. pub fn publish(&mut self, message: Message) { self.messages.push(message); diff --git a/native/src/svg.rs b/native/src/svg.rs index 90eff87e..f86fec5b 100644 --- a/native/src/svg.rs +++ b/native/src/svg.rs @@ -5,7 +5,7 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; -/// An [`Svg`] handle. +/// A handle of Svg data. #[derive(Debug, Clone)] pub struct Handle { id: u64, @@ -55,7 +55,7 @@ impl Hash for Handle { } } -/// The data of an [`Svg`]. +/// The data of a vectorial image. #[derive(Clone, Hash)] pub enum Data { /// File data diff --git a/native/src/system.rs b/native/src/system.rs new file mode 100644 index 00000000..61c8ff29 --- /dev/null +++ b/native/src/system.rs @@ -0,0 +1,6 @@ +//! Access the native system. +mod action; +mod information; + +pub use action::Action; +pub use information::Information; diff --git a/native/src/system/action.rs b/native/src/system/action.rs new file mode 100644 index 00000000..dea9536f --- /dev/null +++ b/native/src/system/action.rs @@ -0,0 +1,39 @@ +use crate::system; + +use iced_futures::MaybeSend; +use std::fmt; + +/// An operation to be performed on the system. +pub enum Action<T> { + /// Query system information and produce `T` with the result. + QueryInformation(Box<dyn Closure<T>>), +} + +pub trait Closure<T>: Fn(system::Information) -> T + MaybeSend {} + +impl<T, O> Closure<O> for T where T: Fn(system::Information) -> O + MaybeSend {} + +impl<T> Action<T> { + /// Maps the output of a system [`Action`] using the provided closure. + pub fn map<A>( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action<A> + where + T: 'static, + { + match self { + Self::QueryInformation(o) => { + Action::QueryInformation(Box::new(move |s| f(o(s)))) + } + } + } +} + +impl<T> fmt::Debug for Action<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::QueryInformation(_) => write!(f, "Action::QueryInformation"), + } + } +} diff --git a/native/src/system/information.rs b/native/src/system/information.rs new file mode 100644 index 00000000..fa4a835b --- /dev/null +++ b/native/src/system/information.rs @@ -0,0 +1,22 @@ +/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). +#[derive(Clone, Debug)] +pub struct Information { + /// Contains the system name. + pub system_name: Option<String>, + /// Contains the kernel version. + pub system_kernel: Option<String>, + /// Contains the systme version. + pub system_version: Option<String>, + /// Contains the processor brand. + pub cpu_brand: String, + /// Contains the number of physical cores on the processor. + pub cpu_cores: Option<usize>, + /// Contains the total RAM size in KB. + pub memory_total: u64, + /// Contains the system used RAM size in KB. + pub memory_used: Option<u64>, + /// Contains the graphics backend. + pub graphics_backend: String, + /// Contains the graphics adapter. + pub graphics_adapter: String, +} diff --git a/native/src/text.rs b/native/src/text.rs index 256a9c5a..6e28681d 100644 --- a/native/src/text.rs +++ b/native/src/text.rs @@ -39,7 +39,7 @@ pub enum Hit { } impl Hit { - /// Computes the cursor position corresponding to this [`HitTestResult`] . + /// Computes the cursor position of the [`Hit`] . pub fn cursor(self) -> usize { match self { Self::CharOffset(i) => i, diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 6fc6a479..344ba4d6 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,8 +1,10 @@ //! Implement your own event loop to drive a user interface. +use crate::application; use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -13,14 +15,16 @@ use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// charge of using this type in your system in any way you want. /// /// # Example -/// The [`integration` example] uses a [`UserInterface`] to integrate Iced in -/// an existing graphical application. +/// The [`integration_opengl`] & [`integration_wgpu`] examples use a +/// [`UserInterface`] to integrate Iced in an existing graphical application. /// -/// [`integration` example]: https://github.com/iced-rs/iced/tree/0.3/examples/integration +/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.4/examples/integration_opengl +/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/examples/integration_wgpu #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, base: layout::Node, + state: widget::Tree, overlay: Option<layout::Node>, bounds: Size, } @@ -28,6 +32,7 @@ pub struct UserInterface<'a, Message, Renderer> { impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: application::StyleSheet, { /// Builds a user interface for an [`Element`]. /// @@ -86,17 +91,21 @@ where pub fn build<E: Into<Element<'a, Message, Renderer>>>( root: E, bounds: Size, - _cache: Cache, + cache: Cache, renderer: &mut Renderer, ) -> Self { let root = root.into(); + let Cache { mut state } = cache; + state.diff(root.as_widget()); + let base = renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)); UserInterface { root, base, + state, overlay: None, bounds, } @@ -177,40 +186,67 @@ where clipboard: &mut dyn Clipboard, messages: &mut Vec<Message>, ) -> (State, Vec<event::Status>) { - let mut state = State::Updated; + use std::mem::ManuallyDrop; - let (base_cursor, overlay_statuses) = if let Some(mut overlay) = - self.root.overlay(Layout::new(&self.base), renderer) - { + let mut state = State::Updated; + let mut manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); + + let (base_cursor, overlay_statuses) = if manual_overlay.is_some() { let bounds = self.bounds; + + let mut overlay = manual_overlay.as_mut().unwrap(); let mut layout = overlay.layout(renderer, bounds); + let mut event_statuses = Vec::new(); + + for event in events.iter().cloned() { + let mut shell = Shell::new(messages); + + let event_status = overlay.on_event( + event, + Layout::new(&layout), + cursor_position, + renderer, + clipboard, + &mut shell, + ); + + event_statuses.push(event_status); - let event_statuses = events - .iter() - .cloned() - .map(|event| { - let mut shell = Shell::new(messages); - - let event_status = overlay.on_event( - event, - Layout::new(&layout), - cursor_position, - renderer, - clipboard, - &mut shell, + if shell.is_layout_invalid() { + let _ = ManuallyDrop::into_inner(manual_overlay); + + self.base = renderer.layout( + &self.root, + &layout::Limits::new(Size::ZERO, self.bounds), ); + manual_overlay = + ManuallyDrop::new(self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + )); + + if manual_overlay.is_none() { + break; + } + + overlay = manual_overlay.as_mut().unwrap(); + shell.revalidate_layout(|| { layout = overlay.layout(renderer, bounds); }); + } - if shell.are_widgets_invalid() { - state = State::Outdated; - } - - event_status - }) - .collect(); + if shell.are_widgets_invalid() { + state = State::Outdated; + } + } let base_cursor = if layout.bounds().contains(cursor_position) { // TODO: Type-safe cursor availability @@ -226,14 +262,21 @@ where (cursor_position, vec![event::Status::Ignored; events.len()]) }; + let _ = ManuallyDrop::into_inner(manual_overlay); + let event_statuses = events .iter() .cloned() .zip(overlay_statuses.into_iter()) .map(|(event, overlay_status)| { + if matches!(overlay_status, event::Status::Captured) { + return overlay_status; + } + let mut shell = Shell::new(messages); - let event_status = self.root.widget.on_event( + let event_status = self.root.as_widget_mut().on_event( + &mut self.state, event, Layout::new(&self.base), base_cursor, @@ -264,19 +307,20 @@ where /// Draws the [`UserInterface`] with the provided [`Renderer`]. /// - /// It returns the some [`Renderer::Output`]. You should update the icon of - /// the mouse cursor accordingly in your system. + /// It returns the current [`mouse::Interaction`]. You should update the + /// icon of the mouse cursor accordingly in your system. /// /// [`Renderer`]: crate::Renderer - /// [`Renderer::Output`]: crate::Renderer::Output /// /// # Example /// We can finally draw our [counter](index.html#usage) by /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::{clipboard, Size, Point}; + /// use iced_native::clipboard; + /// use iced_native::renderer; /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_native::{Size, Point, Theme}; /// use iced_wgpu::Renderer; /// /// # mod iced_wgpu { @@ -323,7 +367,7 @@ where /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position); + /// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position); /// /// cache = user_interface.into_cache(); /// @@ -338,6 +382,8 @@ where pub fn draw( &mut self, renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, cursor_position: Point, ) -> mouse::Interaction { // TODO: Move to shell level (?) @@ -345,9 +391,11 @@ where let viewport = Rectangle::with_size(self.bounds); - let base_cursor = if let Some(overlay) = - self.root.overlay(Layout::new(&self.base), renderer) - { + let base_cursor = if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { let overlay_layout = self .overlay .take() @@ -367,15 +415,18 @@ where cursor_position }; - self.root.widget.draw( + self.root.as_widget().draw( + &self.state, renderer, - &renderer::Style::default(), + theme, + style, Layout::new(&self.base), base_cursor, &viewport, ); - let base_interaction = self.root.widget.mouse_interaction( + let base_interaction = self.root.as_widget().mouse_interaction( + &self.state, Layout::new(&self.base), cursor_position, &viewport, @@ -397,51 +448,79 @@ where overlay .as_ref() .and_then(|layout| { - root.overlay(Layout::new(&base), renderer).map(|overlay| { - let overlay_interaction = overlay.mouse_interaction( - Layout::new(layout), - cursor_position, - &viewport, - renderer, - ); - - let overlay_bounds = layout.bounds(); - - renderer.with_layer(overlay_bounds, |renderer| { - overlay.draw( - renderer, - &renderer::Style::default(), + root.as_widget() + .overlay(&mut self.state, Layout::new(base), renderer) + .map(|overlay| { + let overlay_interaction = overlay.mouse_interaction( Layout::new(layout), cursor_position, + &viewport, + renderer, ); - }); - if overlay_bounds.contains(cursor_position) { - overlay_interaction - } else { - base_interaction - } - }) + let overlay_bounds = layout.bounds(); + + renderer.with_layer(overlay_bounds, |renderer| { + overlay.draw( + renderer, + theme, + style, + Layout::new(layout), + cursor_position, + ); + }); + + if overlay_bounds.contains(cursor_position) { + overlay_interaction + } else { + base_interaction + } + }) }) .unwrap_or(base_interaction) } + /// Applies a [`widget::Operation`] to the [`UserInterface`]. + pub fn operate( + &mut self, + renderer: &Renderer, + operation: &mut dyn widget::Operation<Message>, + ) { + self.root.as_widget().operate( + &mut self.state, + Layout::new(&self.base), + operation, + ); + + if let Some(layout) = self.overlay.as_ref() { + if let Some(overlay) = self.root.as_widget().overlay( + &mut self.state, + Layout::new(&self.base), + renderer, + ) { + overlay.operate(Layout::new(layout), operation); + } + } + } + /// Relayouts and returns a new [`UserInterface`] using the provided /// bounds. pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self { - Self::build(self.root, bounds, Cache, renderer) + Self::build(self.root, bounds, Cache { state: self.state }, renderer) } /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the /// process. pub fn into_cache(self) -> Cache { - Cache + Cache { state: self.state } } } /// Reusable data of a specific [`UserInterface`]. -#[derive(Debug, Clone)] -pub struct Cache; +#[derive(Debug)] +pub struct Cache { + state: widget::Tree, +} impl Cache { /// Creates an empty [`Cache`]. @@ -449,7 +528,9 @@ impl Cache { /// You should use this to initialize a [`Cache`] before building your first /// [`UserInterface`]. pub fn new() -> Cache { - Cache + Cache { + state: widget::Tree::empty(), + } } } diff --git a/native/src/widget.rs b/native/src/widget.rs index aacdc3d9..8890b8e7 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -15,7 +15,9 @@ pub mod button; pub mod checkbox; pub mod column; pub mod container; +pub mod helpers; pub mod image; +pub mod operation; pub mod pane_grid; pub mod pick_list; pub mod progress_bar; @@ -30,6 +32,10 @@ pub mod text; pub mod text_input; pub mod toggler; pub mod tooltip; +pub mod tree; + +mod action; +mod id; #[doc(no_inline)] pub use button::Button; @@ -40,6 +46,8 @@ pub use column::Column; #[doc(no_inline)] pub use container::Container; #[doc(no_inline)] +pub use helpers::*; +#[doc(no_inline)] pub use image::Image; #[doc(no_inline)] pub use pane_grid::PaneGrid; @@ -69,6 +77,12 @@ pub use text_input::TextInput; pub use toggler::Toggler; #[doc(no_inline)] pub use tooltip::Tooltip; +#[doc(no_inline)] +pub use tree::Tree; + +pub use action::Action; +pub use id::Id; +pub use operation::Operation; use crate::event::{self, Event}; use crate::layout; @@ -93,12 +107,12 @@ use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.3/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.3/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.3/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.3/examples/geometry +/// [examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.4/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.4/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.4/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.3/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, @@ -109,12 +123,10 @@ where /// Returns the height of the [`Widget`]. fn height(&self) -> Length; - /// Returns the [`Node`] of the [`Widget`]. + /// Returns the [`layout::Node`] of the [`Widget`]. /// - /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the /// user interface. - /// - /// [`Node`]: layout::Node fn layout( &self, renderer: &Renderer, @@ -124,27 +136,52 @@ where /// Draws the [`Widget`] using the associated `Renderer`. fn draw( &self, + state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ); - /// Processes a runtime [`Event`]. + /// Returns the [`Tag`] of the [`Widget`]. /// - /// It receives: - /// * an [`Event`] describing user interaction - /// * the computed [`Layout`] of the [`Widget`] - /// * the current cursor position - /// * a mutable `Message` list, allowing the [`Widget`] to produce - /// new messages based on user interaction. - /// * the `Renderer` - /// * a [`Clipboard`], if available + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec<Tree> { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _operation: &mut dyn Operation<Message>, + ) { + } + + /// Processes a runtime [`Event`]. /// /// By default, it does nothing. fn on_event( &mut self, + _state: &mut Tree, _event: Event, _layout: Layout<'_>, _cursor_position: Point, @@ -160,6 +197,7 @@ where /// By default, it returns [`mouse::Interaction::Idle`]. fn mouse_interaction( &self, + _state: &Tree, _layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, @@ -169,11 +207,12 @@ where } /// Returns the overlay of the [`Widget`], if there is any. - fn overlay( - &mut self, + fn overlay<'a>( + &'a self, + _state: &'a mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { + ) -> Option<overlay::Element<'a, Message, Renderer>> { None } } diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs new file mode 100644 index 00000000..766e902b --- /dev/null +++ b/native/src/widget/action.rs @@ -0,0 +1,88 @@ +use crate::widget::operation::{self, Operation}; +use crate::widget::Id; + +use iced_futures::MaybeSend; + +/// An operation to be performed on the widget tree. +#[allow(missing_debug_implementations)] +pub struct Action<T>(Box<dyn Operation<T>>); + +impl<T> Action<T> { + /// Creates a new [`Action`] with the given [`Operation`]. + pub fn new(operation: impl Operation<T> + 'static) -> Self { + Self(Box::new(operation)) + } + + /// Maps the output of an [`Action`] using the given function. + pub fn map<A>( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action<A> + where + T: 'static, + A: 'static, + { + Action(Box::new(Map { + operation: self.0, + f: Box::new(f), + })) + } + + /// Consumes the [`Action`] and returns the internal [`Operation`]. + pub fn into_operation(self) -> Box<dyn Operation<T>> { + self.0 + } +} + +#[allow(missing_debug_implementations)] +struct Map<A, B> { + operation: Box<dyn Operation<A>>, + f: Box<dyn Fn(A) -> B>, +} + +impl<A, B> Operation<B> for Map<A, B> +where + A: 'static, + B: 'static, +{ + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + struct MapRef<'a, A, B> { + operation: &'a mut dyn Operation<A>, + f: &'a dyn Fn(A) -> B, + } + + impl<'a, A, B> Operation<B> for MapRef<'a, A, B> { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), + ) { + let Self { operation, f } = self; + + operation.container(id, &mut |operation| { + operate_on_children(&mut MapRef { operation, f }); + }); + } + } + + let Self { operation, f } = self; + + MapRef { + operation: operation.as_mut(), + f, + } + .container(id, operate_on_children); + } + + fn focusable( + &mut self, + state: &mut dyn operation::Focusable, + id: Option<&Id>, + ) { + self.operation.focusable(state, id); + } +} diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 57fdd7d4..6c0b8f6e 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -7,18 +7,18 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; +use crate::widget::Operation; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; -pub use iced_style::button::{Style, StyleSheet}; +pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -27,17 +27,13 @@ pub use iced_style::button::{Style, StyleSheet}; /// ButtonPressed, /// } /// -/// let mut state = button::State::new(); -/// let button = Button::new(&mut state, Text::new("Press me!")) -/// .on_press(Message::ButtonPressed); +/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// /// ``` -/// # use iced_native::widget::{button, Text}; -/// # /// # type Button<'a, Message> = /// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; /// # @@ -46,48 +42,42 @@ pub use iced_style::button::{Style, StyleSheet}; /// ButtonPressed, /// } /// -/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> { -/// Button::new(state, Text::new("I'm disabled!")) +/// fn disabled_button<'a>() -> Button<'a, Message> { +/// Button::new("I'm disabled!") /// } /// -/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> { -/// disabled_button(state).on_press(Message::ButtonPressed) +/// fn enabled_button<'a>() -> Button<'a, Message> { +/// disabled_button().on_press(Message::ButtonPressed) /// } /// ``` #[allow(missing_debug_implementations)] -pub struct Button<'a, Message, Renderer> { - state: &'a mut State, +pub struct Button<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ content: Element<'a, Message, Renderer>, on_press: Option<Message>, width: Length, height: Length, - min_width: u32, - min_height: u32, padding: Padding, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where - Message: Clone, Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { - /// Creates a new [`Button`] with some local [`State`] and the given - /// content. - pub fn new<E>(state: &'a mut State, content: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + /// Creates a new [`Button`] with the given content. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Button { - state, content: content.into(), on_press: None, width: Length::Shrink, height: Length::Shrink, - min_width: 0, - min_height: 0, padding: Padding::new(5), - style_sheet: Default::default(), + style: <Renderer::Theme as StyleSheet>::Style::default(), } } @@ -103,18 +93,6 @@ where self } - /// Sets the minimum width of the [`Button`]. - pub fn min_width(mut self, min_width: u32) -> Self { - self.min_width = min_width; - self - } - - /// Sets the minimum height of the [`Button`]. - pub fn min_height(mut self, min_height: u32) -> Self { - self.min_height = min_height; - self - } - /// Sets the [`Padding`] of the [`Button`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -122,41 +100,46 @@ where } /// Sets the message that will be produced when the [`Button`] is pressed. - /// If on_press isn't set, button will be disabled. + /// + /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, msg: Message) -> Self { self.on_press = Some(msg); self } - /// Sets the style of the [`Button`]. + /// Sets the style variant of this [`Button`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: <Renderer::Theme as StyleSheet>::Style, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style; self } } -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where - Message: Clone, - Renderer: crate::Renderer, + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn width(&self) -> Length { self.width } @@ -170,26 +153,36 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .min_width(self.min_width) - .min_height(self.min_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - - let size = limits.resolve(content.size()).pad(self.padding); - - layout::Node::with_children(size, vec![content]) + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -197,7 +190,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let event::Status::Captured = self.content.on_event( + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], event.clone(), layout.children().next().unwrap(), cursor_position, @@ -208,64 +202,21 @@ where return event::Status::Captured; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if self.on_press.is_some() { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - self.state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { - let bounds = layout.bounds(); - - if self.state.is_pressed { - self.state.is_pressed = false; - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - if is_mouse_over && !is_disabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || tree.state.downcast_mut::<State>(), + ) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -274,54 +225,20 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); - let is_mouse_over = bounds.contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - let styling = if is_disabled { - self.style_sheet.disabled() - } else if is_mouse_over { - if self.state.is_pressed { - self.style_sheet.pressed() - } else { - self.style_sheet.hovered() - } - } else { - self.style_sheet.active() - }; - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - border_radius: styling.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: styling.border_radius, - border_width: styling.border_width, - border_color: styling.border_color, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } + let styling = draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + theme, + self.style, + || tree.state.downcast_ref::<State>(), + ); - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, + theme, &renderer::Style { text_color: styling.text_color, }, @@ -331,25 +248,205 @@ where ); } - fn overlay( - &mut self, + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position, self.on_press.is_some()) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.content - .overlay(layout.children().next().unwrap(), renderer) + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) } } impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, + Message: Clone + 'a, + Renderer: crate::Renderer + 'a, + Renderer::Theme: StyleSheet, { - fn from( - button: Button<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(button) + fn from(button: Button<'a, Message, Renderer>) -> Self { + Self::new(button) + } +} + +/// The local state of a [`Button`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_pressed: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +pub fn update<'a, Message: Clone>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_press: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if on_press.is_some() { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let state = state(); + + state.is_pressed = true; + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = state(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: crate::Renderer>( + renderer: &mut Renderer, + bounds: Rectangle, + cursor_position: Point, + is_enabled: bool, + style_sheet: &dyn StyleSheet< + Style = <Renderer::Theme as StyleSheet>::Style, + >, + style: <Renderer::Theme as StyleSheet>::Style, + state: impl FnOnce() -> &'a State, +) -> Appearance +where + Renderer::Theme: StyleSheet, +{ + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if !is_enabled { + style_sheet.disabled(style) + } else if is_mouse_over { + let state = state(); + + if state.is_pressed { + style_sheet.pressed(style) + } else { + style_sheet.hovered(style) + } + } else { + style_sheet.active(style) + }; + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + border_radius: styling.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + styling +} + +/// Computes the layout of a [`Button`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height).pad(padding); + + let mut content = layout_content(renderer, &limits); + content.move_to(Point::new(padding.left.into(), padding.top.into())); + + let size = limits.resolve(content.size()).pad(padding); + + layout::Node::with_children(size, vec![content]) +} + +/// Returns the [`mouse::Interaction`] of a [`Button`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + is_enabled: bool, +) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + if is_mouse_over && is_enabled { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() } } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 15cbf93a..dc3c0bd0 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -6,13 +6,13 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, }; -pub use iced_style::checkbox::{Style, StyleSheet}; +pub use iced_style::checkbox::{Appearance, StyleSheet}; /// A box that can be checked. /// @@ -32,19 +32,27 @@ pub use iced_style::checkbox::{Style, StyleSheet}; /// ///  #[allow(missing_debug_implementations)] -pub struct Checkbox<'a, Message, Renderer: text::Renderer> { +pub struct Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, +{ is_checked: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: String, width: Length, size: u16, spacing: u16, text_size: Option<u16>, font: Renderer::Font, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { +impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, +{ /// The default size of a [`Checkbox`]. const DEFAULT_SIZE: u16 = 20; @@ -61,7 +69,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// `Message`. pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Checkbox { is_checked, @@ -72,7 +80,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { spacing: Self::DEFAULT_SPACING, text_size: None, font: Renderer::Font::default(), - style_sheet: Default::default(), + style: Default::default(), } } @@ -102,7 +110,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// Sets the [`Font`] of the text of the [`Checkbox`]. /// - /// [`Font`]: crate::widget::text::Renderer::Font + /// [`Font`]: crate::text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self @@ -111,9 +119,9 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// Sets the style of the [`Checkbox`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } @@ -122,6 +130,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> for Checkbox<'a, Message, Renderer> where Renderer: text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { fn width(&self) -> Length { self.width @@ -149,13 +158,17 @@ where Text::new(&self.label) .font(self.font.clone()) .width(self.width) - .size(self.text_size.unwrap_or(renderer.default_size())), + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ), ) .layout(renderer, limits) } fn on_event( &mut self, + _tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -182,6 +195,7 @@ where fn mouse_interaction( &self, + _tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -196,7 +210,9 @@ where fn draw( &self, + _tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -208,9 +224,9 @@ where let mut children = layout.children(); let custom_style = if is_mouse_over { - self.style_sheet.hovered(self.is_checked) + theme.hovered(self.style, self.is_checked) } else { - self.style_sheet.active(self.is_checked) + theme.active(self.style, self.is_checked) }; { @@ -252,9 +268,11 @@ where style, label_layout, &self.label, - self.font.clone(), self.text_size, - custom_style.text_color, + self.font.clone(), + widget::text::Appearance { + color: custom_style.text_color, + }, alignment::Horizontal::Left, alignment::Vertical::Center, ); @@ -265,8 +283,9 @@ where impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + text::Renderer, Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { fn from( checkbox: Checkbox<'a, Message, Renderer>, diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index f161d1f2..a8b0f183 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -4,6 +4,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -19,7 +20,6 @@ pub struct Column<'a, Message, Renderer> { width: Length, height: Length, max_width: u32, - max_height: u32, align_items: Alignment, children: Vec<Element<'a, Message, Renderer>>, } @@ -40,7 +40,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { width: Length::Shrink, height: Length::Shrink, max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -48,7 +47,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { /// Sets the vertical spacing _between_ elements. /// - /// Custom margins per element do not exist in Iced. You should use this + /// Custom margins per element do not exist in iced. You should use this /// method instead! While less flexible, it helps you keep spacing between /// elements consistent. pub fn spacing(mut self, units: u16) -> Self { @@ -80,12 +79,6 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { self } - /// Sets the maximum height of the [`Column`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the horizontal alignment of the contents of the [`Column`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,20 +86,34 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { } /// Adds an element to the [`Column`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { self.children.push(child.into()); self } } +impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { + fn default() -> Self { + Self::new() + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Column<'a, Message, Renderer> where Renderer: crate::Renderer, { + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + fn width(&self) -> Length { self.width } @@ -122,7 +129,6 @@ where ) -> layout::Node { let limits = limits .max_width(self.max_width) - .max_height(self.max_height) .width(self.width) .height(self.height); @@ -137,8 +143,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -148,9 +172,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -164,6 +190,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -171,9 +198,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -186,41 +215,49 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, style, layout, cursor_position, 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_position, + viewport, + ); } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from( - column: Column<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) + fn from(column: Column<'a, Message, Renderer>) -> Self { + Self::new(column) } } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index ca85a425..2afad3f2 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -5,6 +5,7 @@ use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Widget, @@ -12,13 +13,17 @@ use crate::{ use std::u32; -pub use iced_style::container::{Style, StyleSheet}; +pub use iced_style::container::{Appearance, StyleSheet}; /// An element decorating some content. /// /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer> { +pub struct Container<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ padding: Padding, width: Length, height: Length, @@ -26,13 +31,14 @@ pub struct Container<'a, Message, Renderer> { max_height: u32, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, content: Element<'a, Message, Renderer>, } impl<'a, Message, Renderer> Container<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { /// Creates an empty [`Container`]. pub fn new<T>(content: T) -> Self @@ -47,7 +53,7 @@ where max_height: u32::MAX, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - style_sheet: Default::default(), + style: Default::default(), content: content.into(), } } @@ -109,9 +115,9 @@ where /// Sets the style of the [`Container`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } @@ -120,7 +126,16 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn width(&self) -> Length { self.width } @@ -134,32 +149,40 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .loose() - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits.loose()); - let size = limits.resolve(content.size()); - - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - content.align( - Alignment::from(self.horizontal_alignment), - Alignment::from(self.vertical_alignment), - size, - ); + layout( + renderer, + limits, + self.width, + self.height, + self.max_width, + self.max_height, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } - layout::Node::with_children(size.pad(self.padding), vec![content]) + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -167,7 +190,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.content.widget.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, layout.children().next().unwrap(), cursor_position, @@ -179,12 +203,14 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content.widget.mouse_interaction( + self.content.as_widget().mouse_interaction( + &tree.children[0], layout.children().next().unwrap(), cursor_position, viewport, @@ -194,18 +220,22 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - let style = self.style_sheet.style(); + let style = theme.appearance(self.style); draw_background(renderer, &style, layout.bounds()); - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, + theme, &renderer::Style { text_color: style .text_color @@ -217,48 +247,87 @@ where ); } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.content - .overlay(layout.children().next().unwrap(), renderer) + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + ) } } +impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + column: Container<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(column) + } +} + +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_width: u32, + max_height: u32, + padding: Padding, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits + .loose() + .max_width(max_width) + .max_height(max_height) + .width(width) + .height(height) + .pad(padding); + + let mut content = layout_content(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left.into(), padding.top.into())); + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ); + + layout::Node::with_children(size.pad(padding), vec![content]) +} + /// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. pub fn draw_background<Renderer>( renderer: &mut Renderer, - style: &Style, + appearance: &Appearance, bounds: Rectangle, ) where Renderer: crate::Renderer, { - if style.background.is_some() || style.border_width > 0.0 { + if appearance.background.is_some() || appearance.border_width > 0.0 { renderer.fill_quad( renderer::Quad { bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, }, - style + appearance .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); } } - -impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs new file mode 100644 index 00000000..79751878 --- /dev/null +++ b/native/src/widget/helpers.rs @@ -0,0 +1,283 @@ +//! Helper functions to create pure widgets. +use crate::widget; +use crate::{Element, Length}; + +use std::borrow::Cow; +use std::ops::RangeInclusive; + +/// Creates a [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +#[macro_export] +macro_rules! column { + () => ( + $crate::widget::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + +/// Creates a [Row`] with the given children. +/// +/// [`Row`]: widget::Row +#[macro_export] +macro_rules! row { + () => ( + $crate::widget::Row::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+]) + ); +} + +/// Creates a new [`Container`] with the provided content. +/// +/// [`Container`]: widget::Container +pub fn container<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Container<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::container::StyleSheet, +{ + widget::Container::new(content) +} + +/// Creates a new [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +pub fn column<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> widget::Column<'_, Message, Renderer> { + widget::Column::with_children(children) +} + +/// Creates a new [`Row`] with the given children. +/// +/// [`Row`]: widget::Row +pub fn row<Message, Renderer>( + children: Vec<Element<'_, Message, Renderer>>, +) -> widget::Row<'_, Message, Renderer> { + widget::Row::with_children(children) +} + +/// Creates a new [`Scrollable`] with the provided content. +/// +/// [`Scrollable`]: widget::Scrollable +pub fn scrollable<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::scrollable::StyleSheet, +{ + widget::Scrollable::new(content) +} + +/// Creates a new [`Button`] with the provided content. +/// +/// [`Button`]: widget::Button +pub fn button<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, +) -> widget::Button<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::button::StyleSheet, +{ + widget::Button::new(content) +} + +/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`]. +/// +/// [`Tooltip`]: widget::Tooltip +/// [`tooltip::Position`]: widget::tooltip::Position +pub fn tooltip<'a, Message, Renderer>( + content: impl Into<Element<'a, Message, Renderer>>, + tooltip: impl ToString, + position: widget::tooltip::Position, +) -> widget::Tooltip<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet, +{ + widget::Tooltip::new(content, tooltip.to_string(), position) +} + +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: widget::Text +pub fn text<'a, Renderer>(text: impl ToString) -> widget::Text<'a, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::text::StyleSheet, +{ + widget::Text::new(text.to_string()) +} + +/// Creates a new [`Checkbox`]. +/// +/// [`Checkbox`]: widget::Checkbox +pub fn checkbox<'a, Message, Renderer>( + label: impl Into<String>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Checkbox<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, +{ + widget::Checkbox::new(is_checked, label, f) +} + +/// Creates a new [`Radio`]. +/// +/// [`Radio`]: widget::Radio +pub fn radio<Message, Renderer, V>( + label: impl Into<String>, + value: V, + selected: Option<V>, + on_click: impl FnOnce(V) -> Message, +) -> widget::Radio<Message, Renderer> +where + Message: Clone, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::radio::StyleSheet, + V: Copy + Eq, +{ + widget::Radio::new(value, label, selected, on_click) +} + +/// Creates a new [`Toggler`]. +/// +/// [`Toggler`]: widget::Toggler +pub fn toggler<'a, Message, Renderer>( + label: impl Into<Option<String>>, + is_checked: bool, + f: impl Fn(bool) -> Message + 'a, +) -> widget::Toggler<'a, Message, Renderer> +where + Renderer: crate::text::Renderer, + Renderer::Theme: widget::toggler::StyleSheet, +{ + widget::Toggler::new(is_checked, label, f) +} + +/// Creates a new [`TextInput`]. +/// +/// [`TextInput`]: widget::TextInput +pub fn text_input<'a, Message, Renderer>( + placeholder: &str, + value: &str, + on_change: impl Fn(String) -> Message + 'a, +) -> widget::TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::text_input::StyleSheet, +{ + widget::TextInput::new(placeholder, value, on_change) +} + +/// Creates a new [`Slider`]. +/// +/// [`Slider`]: widget::Slider +pub fn slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive<T>, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::Slider<'a, T, Message, Renderer> +where + T: Copy + From<u8> + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: widget::slider::StyleSheet, +{ + widget::Slider::new(range, value, on_change) +} + +/// Creates a new [`PickList`]. +/// +/// [`PickList`]: widget::PickList +pub fn pick_list<'a, Message, Renderer, T>( + options: impl Into<Cow<'a, [T]>>, + selected: Option<T>, + on_selected: impl Fn(T) -> Message + 'a, +) -> widget::PickList<'a, T, Message, Renderer> +where + T: ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Renderer: crate::text::Renderer, + Renderer::Theme: widget::pick_list::StyleSheet, +{ + widget::PickList::new(options, selected, on_selected) +} + +/// Creates a new [`Image`]. +/// +/// [`Image`]: widget::Image +pub fn image<Handle>(handle: impl Into<Handle>) -> widget::Image<Handle> { + widget::Image::new(handle.into()) +} + +/// Creates a new horizontal [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn horizontal_space(width: Length) -> widget::Space { + widget::Space::with_width(width) +} + +/// Creates a new vertical [`Space`] with the given [`Length`]. +/// +/// [`Space`]: widget::Space +pub fn vertical_space(height: Length) -> widget::Space { + widget::Space::with_height(height) +} + +/// Creates a horizontal [`Rule`] with the given height. +/// +/// [`Rule`]: widget::Rule +pub fn horizontal_rule<Renderer>(height: u16) -> widget::Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::rule::StyleSheet, +{ + widget::Rule::horizontal(height) +} + +/// Creates a vertical [`Rule`] with the given width. +/// +/// [`Rule`]: widget::Rule +pub fn vertical_rule<Renderer>(width: u16) -> widget::Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::rule::StyleSheet, +{ + widget::Rule::vertical(width) +} + +/// Creates a new [`ProgressBar`]. +/// +/// It expects: +/// * an inclusive range of possible values, and +/// * the current value of the [`ProgressBar`]. +/// +/// [`ProgressBar`]: widget::ProgressBar +pub fn progress_bar<Renderer>( + range: RangeInclusive<f32>, + value: f32, +) -> widget::ProgressBar<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: widget::progress_bar::StyleSheet, +{ + widget::ProgressBar::new(range, value) +} + +/// Creates a new [`Svg`] widget from the given [`Handle`]. +/// +/// [`Svg`]: widget::Svg +/// [`Handle`]: widget::svg::Handle +pub fn svg(handle: impl Into<widget::svg::Handle>) -> widget::Svg { + widget::Svg::new(handle) +} diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs new file mode 100644 index 00000000..4b8fedf1 --- /dev/null +++ b/native/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index de0ffbc0..91d68e34 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -5,12 +5,18 @@ pub use viewer::Viewer; use crate::image; use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::hash::Hash; +/// Creates a new [`Viewer`] with the given image `Handle`. +pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { + Viewer::new(handle) +} + /// A frame that displays an image while keeping aspect ratio. /// /// # Example @@ -65,6 +71,46 @@ impl<Handle> Image<Handle> { } } +/// Computes the layout of an [`Image`]. +pub fn layout<Renderer, Handle>( + renderer: &Renderer, + limits: &layout::Limits, + handle: &Handle, + width: Length, + height: Length, + content_fit: ContentFit, +) -> layout::Node +where + Renderer: image::Renderer<Handle = Handle>, +{ + // The raw w/h of the underlying image + let image_size = { + let (width, height) = renderer.dimensions(handle); + + 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(width).height(height).resolve(image_size); + + // The uncropped size of the image when fit to the bounds above + let full_size = content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) +} + impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, @@ -83,37 +129,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - // The raw w/h of the underlying image - let (width, height) = renderer.dimensions(&self.handle); - 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); - - // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); - - // Shrink the widget to fit the resized image, if requested - let final_size = Size { - width: match self.width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match self.height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) + layout( + renderer, + limits, + &self.handle, + self.width, + self.height, + self.content_fit, + ) } fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 840b88e5..b1fe596c 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -4,6 +4,7 @@ use crate::image; use crate::layout; use crate::mouse; use crate::renderer; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, @@ -13,8 +14,7 @@ use std::hash::Hash; /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] -pub struct Viewer<'a, Handle> { - state: &'a mut State, +pub struct Viewer<Handle> { padding: u16, width: Length, height: Length, @@ -24,11 +24,10 @@ pub struct Viewer<'a, Handle> { handle: Handle, } -impl<'a, Handle> Viewer<'a, Handle> { +impl<Handle> Viewer<Handle> { /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(state: &'a mut State, handle: Handle) -> Self { + pub fn new(handle: Handle) -> Self { Viewer { - state, padding: 0, width: Length::Shrink, height: Length::Shrink, @@ -81,43 +80,21 @@ impl<'a, Handle> Viewer<'a, Handle> { self.scale_step = scale_step; self } - - /// Returns the bounds of the underlying image, given the bounds of - /// the [`Viewer`]. Scaling will be applied and original aspect ratio - /// will be respected. - fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size - where - Renderer: image::Renderer<Handle = Handle>, - { - let (width, height) = renderer.dimensions(&self.handle); - - let (width, height) = { - let dimensions = (width as f32, height as f32); - - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - - let scale = self.state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) - } } -impl<'a, Message, Renderer, Handle> Widget<Message, Renderer> - for Viewer<'a, Handle> +impl<Message, Renderer, Handle> Widget<Message, Renderer> for Viewer<Handle> where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + fn width(&self) -> Length { self.width } @@ -164,6 +141,7 @@ where fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -181,39 +159,43 @@ where match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - let previous_scale = self.state.scale; + let state = tree.state.downcast_mut::<State>(); + let previous_scale = state.scale; if y < 0.0 && previous_scale > self.min_scale || y > 0.0 && previous_scale < self.max_scale { - self.state.scale = (if y > 0.0 { - self.state.scale * (1.0 + self.scale_step) + state.scale = (if y > 0.0 { + state.scale * (1.0 + self.scale_step) } else { - self.state.scale / (1.0 + self.scale_step) + state.scale / (1.0 + self.scale_step) }) .max(self.min_scale) .min(self.max_scale); - let image_size = - self.image_size(renderer, bounds.size()); + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); - let factor = - self.state.scale / previous_scale - 1.0; + let factor = state.scale / previous_scale - 1.0; let cursor_to_center = cursor_position - bounds.center(); let adjustment = cursor_to_center * factor - + self.state.current_offset * factor; + + state.current_offset * factor; - self.state.current_offset = Vector::new( + state.current_offset = Vector::new( if image_size.width > bounds.width { - self.state.current_offset.x + adjustment.x + state.current_offset.x + adjustment.x } else { 0.0 }, if image_size.height > bounds.height { - self.state.current_offset.y + adjustment.y + state.current_offset.y + adjustment.y } else { 0.0 }, @@ -227,21 +209,34 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if is_mouse_over => { - self.state.cursor_grabbed_at = Some(cursor_position); - self.state.starting_offset = self.state.current_offset; + let state = tree.state.downcast_mut::<State>(); + + state.cursor_grabbed_at = Some(cursor_position); + state.starting_offset = state.current_offset; event::Status::Captured } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - if self.state.cursor_grabbed_at.is_some() => - { - self.state.cursor_grabbed_at = None; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree.state.downcast_mut::<State>(); - event::Status::Captured + if state.cursor_grabbed_at.is_some() { + state.cursor_grabbed_at = None; + + event::Status::Captured + } else { + event::Status::Ignored + } } Event::Mouse(mouse::Event::CursorMoved { position }) => { - if let Some(origin) = self.state.cursor_grabbed_at { - let image_size = self.image_size(renderer, bounds.size()); + let state = tree.state.downcast_mut::<State>(); + + if let Some(origin) = state.cursor_grabbed_at { + let image_size = image_size( + renderer, + &self.handle, + state, + bounds.size(), + ); let hidden_width = (image_size.width - bounds.width / 2.0) .max(0.0) @@ -255,7 +250,7 @@ where let delta = position - origin; let x = if bounds.width < image_size.width { - (self.state.starting_offset.x - delta.x) + (state.starting_offset.x - delta.x) .min(hidden_width) .max(-hidden_width) } else { @@ -263,14 +258,14 @@ where }; let y = if bounds.height < image_size.height { - (self.state.starting_offset.y - delta.y) + (state.starting_offset.y - delta.y) .min(hidden_height) .max(-hidden_height) } else { 0.0 }; - self.state.current_offset = Vector::new(x, y); + state.current_offset = Vector::new(x, y); event::Status::Captured } else { @@ -283,15 +278,17 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); - if self.state.is_cursor_grabbed() { + if state.is_cursor_grabbed() { mouse::Interaction::Grabbing } else if is_mouse_over { mouse::Interaction::Grab @@ -302,15 +299,19 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let image_size = self.image_size(renderer, bounds.size()); + let image_size = + image_size(renderer, &self.handle, state, bounds.size()); let translation = { let image_top_left = Vector::new( @@ -318,7 +319,7 @@ where bounds.height / 2.0 - image_size.height / 2.0, ); - image_top_left - self.state.offset(bounds, image_size) + image_top_left - state.offset(bounds, image_size) }; renderer.with_layer(bounds, |renderer| { @@ -384,14 +385,47 @@ impl State { } } -impl<'a, Message, Renderer, Handle> From<Viewer<'a, Handle>> +impl<'a, Message, Renderer, Handle> From<Viewer<Handle>> for Element<'a, Message, Renderer> where Renderer: 'a + image::Renderer<Handle = Handle>, Message: 'a, Handle: Clone + Hash + 'a, { - fn from(viewer: Viewer<'a, Handle>) -> Element<'a, Message, Renderer> { + fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Renderer> { Element::new(viewer) } } + +/// Returns the bounds of the underlying image, given the bounds of +/// the [`Viewer`]. Scaling will be applied and original aspect ratio +/// will be respected. +pub fn image_size<Renderer>( + renderer: &Renderer, + handle: &<Renderer as image::Renderer>::Handle, + state: &State, + bounds: Size, +) -> Size +where + Renderer: image::Renderer, +{ + let (width, height) = renderer.dimensions(handle); + + let (width, height) = { + let dimensions = (width as f32, height as f32); + + let width_ratio = bounds.width / dimensions.0; + let height_ratio = bounds.height / dimensions.1; + + let ratio = width_ratio.min(height_ratio); + let scale = state.scale; + + if ratio < 1.0 { + (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) + } else { + (dimensions.0 * scale, dimensions.1 * scale) + } + }; + + Size::new(width, height) +} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs new file mode 100644 index 00000000..ef636aa2 --- /dev/null +++ b/native/src/widget/operation.rs @@ -0,0 +1,60 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; + +use crate::widget::Id; + +use std::fmt; + +/// A piece of logic that can traverse the widget tree of an application in +/// order to query or update some widget state. +pub trait Operation<T> { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome<T> { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome<T> { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box<dyn Operation<T>>), +} + +impl<T> fmt::Debug for Outcome<T> +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({:?})", output), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs new file mode 100644 index 00000000..f17bf178 --- /dev/null +++ b/native/src/widget/operation/focusable.rs @@ -0,0 +1,169 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + focused: Option<usize>, + + /// The total amount of focusable widgets. + total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus<T>(target: Id) -> impl Operation<T> { + struct Focus { + target: Id, + } + + impl<T> Operation<T> for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// Produces an [`Operation`] that generates a [`Count`] and chains it with the +/// provided function to build a new [`Operation`]. +pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T> +where + O: Operation<T> + 'static, +{ + struct CountFocusable<O> { + count: Count, + next: fn(Count) -> O, + } + + impl<T, O> Operation<T> for CountFocusable<O> + where + O: Operation<T> + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome<T> { + Outcome::Chain(Box::new((self.next)(self.count))) + } + } + + CountFocusable { + count: Count::default(), + next: f, + } +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the previous focusable widget. +/// - if not found, focuses the last focusable widget. +pub fn focus_previous<T>() -> impl Operation<T> { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + count(|count| FocusPrevious { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the next focusable widget. +/// - if not found, focuses the first focusable widget. +pub fn focus_next<T>() -> impl Operation<T> { + struct FocusNext { + count: Count, + current: usize, + } + + impl<T> Operation<T> for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..2210137d --- /dev/null +++ b/native/src/widget/operation/scrollable.rs @@ -0,0 +1,35 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage`. + fn snap_to(&mut self, percentage: f32); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to<T>(target: Id, percentage: f32) -> impl Operation<T> { + struct SnapTo { + target: Id, + percentage: f32, + } + + impl<T> Operation<T> for SnapTo { + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.percentage); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), + ) { + operate_on_children(self) + } + } + + SnapTo { target, percentage } +} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 8ad63cf1..d84fb7a0 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,40 +6,45 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid mod axis; mod configuration; mod content; mod direction; +mod draggable; mod node; mod pane; mod split; -mod state; mod title_bar; +pub mod state; + pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; pub use direction::Direction; +pub use draggable::Draggable; pub use node::Node; pub use pane::Pane; pub use split::Split; pub use state::State; pub use title_bar::TitleBar; +pub use iced_style::pane_grid::{Line, StyleSheet}; + use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; +use crate::widget::container; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_style::pane_grid::{Line, StyleSheet}; - /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// @@ -62,7 +67,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// ## Example /// /// ``` -/// # use iced_native::widget::{pane_grid, Text}; +/// # use iced_native::widget::{pane_grid, text}; /// # /// # type PaneGrid<'a, Message> = /// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; @@ -80,18 +85,22 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; /// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); /// /// let pane_grid = -/// PaneGrid::new(&mut state, |pane, state| { +/// PaneGrid::new(&state, |pane, state| { /// pane_grid::Content::new(match state { -/// PaneState::SomePane => Text::new("This is some pane"), -/// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"), +/// PaneState::SomePane => text("This is some pane"), +/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), /// }) /// }) /// .on_drag(Message::PaneDragged) /// .on_resize(10, Message::PaneResized); /// ``` #[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> { - state: &'a mut state::Internal, +pub struct PaneGrid<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + state: &'a state::Internal, elements: Vec<(Pane, Content<'a, Message, Renderer>)>, width: Length, height: Length, @@ -99,39 +108,40 @@ pub struct PaneGrid<'a, Message, Renderer> { on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// /// The view function will be called to display each [`Pane`] present in the /// [`State`]. pub fn new<T>( - state: &'a mut State<T>, - view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>, + state: &'a State<T>, + view: impl Fn(Pane, &'a T) -> Content<'a, Message, Renderer>, ) -> Self { let elements = { state .panes - .iter_mut() + .iter() .map(|(pane, pane_state)| (*pane, view(*pane, pane_state))) .collect() }; Self { - state: &mut state.internal, elements, + state: &state.internal, width: Length::Fill, height: Length::Fill, spacing: 0, on_click: None, on_drag: None, on_resize: None, - style_sheet: Default::default(), + style: Default::default(), } } @@ -191,133 +201,44 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style(mut self, style: impl Into<Box<dyn StyleSheet + 'a>>) -> Self { - self.style_sheet = style.into(); + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); self } } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> + for PaneGrid<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, { - fn click_pane( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) { - let mut clicked_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| layout.bounds().contains(cursor_position), - ); - - if let Some(((pane, content), layout)) = clicked_region.next() { - if let Some(on_click) = &self.on_click { - shell.publish(on_click(*pane)); - } - - if let Some(on_drag) = &self.on_drag { - if content.can_be_picked_at(layout, cursor_position) { - let pane_position = layout.position(); - - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); - - self.state.pick_pane(pane, origin); - - shell.publish(on_drag(DragEvent::Picked { pane: *pane })); - } - } - } + fn tag(&self) -> tree::Tag { + tree::Tag::of::<state::Action>() } - fn trigger_resize( - &mut self, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let Some((_, on_resize)) = &self.on_resize { - if let Some((split, _)) = self.state.picked_split() { - let bounds = layout.bounds(); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - if let Some((axis, rectangle, _)) = splits.get(&split) { - let ratio = match axis { - Axis::Horizontal => { - let position = - cursor_position.y - bounds.y - rectangle.y; - - (position / rectangle.height).max(0.1).min(0.9) - } - Axis::Vertical => { - let position = - cursor_position.x - bounds.x - rectangle.x; - - (position / rectangle.width).max(0.1).min(0.9) - } - }; - - shell.publish(on_resize(ResizeEvent { split, ratio })); - - return event::Status::Captured; - } - } - } - - event::Status::Ignored + fn state(&self) -> tree::State { + tree::State::new(state::Action::Idle) } -} - -/// An event produced during a drag and drop interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub enum DragEvent { - /// A [`Pane`] was picked for dragging. - Picked { - /// The picked [`Pane`]. - pane: Pane, - }, - - /// A [`Pane`] was dropped on top of another [`Pane`]. - Dropped { - /// The picked [`Pane`]. - pane: Pane, - - /// The [`Pane`] where the picked one was dropped on. - target: Pane, - }, - - /// A [`Pane`] was picked and then dropped outside of other [`Pane`] - /// boundaries. - Canceled { - /// The picked [`Pane`]. - pane: Pane, - }, -} -/// An event produced during a resize interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub struct ResizeEvent { - /// The [`Split`] that is being dragged for resizing. - pub split: Split, + fn children(&self) -> Vec<Tree> { + self.elements + .iter() + .map(|(_, content)| content.state()) + .collect() + } - /// The new ratio of the [`Split`]. - /// - /// The ratio is a value in [0, 1], representing the exact position of a - /// [`Split`] between two panes. - pub ratio: f32, -} + fn diff(&self, tree: &mut Tree) { + tree.diff_children_custom( + &self.elements, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ fn width(&self) -> Length { self.width } @@ -331,32 +252,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - let regions = self.state.pane_regions(f32::from(self.spacing), size); - - let children = self - .elements - .iter() - .filter_map(|(pane, element)| { - let region = regions.get(pane)?; - let size = Size::new(region.width, region.height); - - let mut node = - element.layout(renderer, &layout::Limits::new(size, size)); - - node.move_to(Point::new(region.x, region.y)); - - Some(node) - }) - .collect(); - - layout::Node::with_children(size, children) + layout( + renderer, + limits, + self.state, + self.width, + self.height, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + |element, renderer, limits| element.layout(renderer, limits), + ) } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -364,97 +274,33 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - event_status = event::Status::Captured; - - match self.on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = self.state.split_regions( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - self.state.pick_split(&split, axis); - } else { - self.click_pane(layout, cursor_position, shell); - } - } - None => { - self.click_pane(layout, cursor_position, shell); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, _)) = self.state.picked_pane() { - if let Some(on_drag) = &self.on_drag { - let mut dropped_region = - self.elements.iter().zip(layout.children()).filter( - |(_, layout)| { - layout.bounds().contains(cursor_position) - }, - ); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != *target => { - DragEvent::Dropped { - pane, - target: *target, - } - } - _ => DragEvent::Canceled { pane }, - }; - - shell.publish(on_drag(event)); - } - - self.state.idle(); - - event_status = event::Status::Captured; - } else if self.state.picked_split().is_some() { - self.state.idle(); - - event_status = event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - event_status = - self.trigger_resize(layout, cursor_position, shell); - } - _ => {} - } - - let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); + let action = tree.state.downcast_mut::<state::Action>(); + + let event_status = update( + action, + self.state, + &event, + layout, + cursor_position, + shell, + self.spacing, + self.elements.iter().map(|(pane, content)| (*pane, content)), + &self.on_click, + &self.on_drag, + &self.on_resize, + ); + + let picked_pane = action.picked_pane().map(|(pane, _)| pane); self.elements .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|((pane, content), layout)| { + .map(|(((pane, content), tree), layout)| { let is_picked = picked_pane == Some(*pane); content.on_event( + tree, event.clone(), layout, cursor_position, @@ -469,229 +315,560 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - if self.state.picked_pane().is_some() { - return mouse::Interaction::Grab; - } - - let resize_axis = - self.state.picked_split().map(|(_, axis)| axis).or_else(|| { - self.on_resize.as_ref().and_then(|(leeway, _)| { - let bounds = layout.bounds(); - - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, + mouse_interaction( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + ) + .unwrap_or_else(|| { + self.elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, ) - .map(|(_, axis, _)| axis) }) - }); - - if let Some(resize_axis) = resize_axis { - return match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }; - } - - self.elements - .iter() - .zip(layout.children()) - .map(|((_pane, content), layout)| { - content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() + .max() + .unwrap_or_default() + }) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - let picked_pane = self.state.picked_pane(); + draw( + tree.state.downcast_ref(), + self.state, + layout, + cursor_position, + renderer, + theme, + style, + viewport, + self.spacing, + self.on_resize.as_ref().map(|(leeway, _)| *leeway), + self.style, + self.elements + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (*pane, (content, tree))), + |(content, tree), + renderer, + style, + layout, + cursor_position, + rectangle| { + content.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + rectangle, + ); + }, + ) + } - let picked_split = self - .state - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'_, Message, Renderer>> { + self.elements + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|(((_, pane), tree), layout)| { + pane.overlay(tree, layout, renderer) + }) + .next() + } +} - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); +impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet + container::StyleSheet, +{ + fn from( + pane_grid: PaneGrid<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(pane_grid) + } +} - let (_axis, region, ratio) = splits.get(&split)?; +/// Calculates the [`Layout`] of a [`PaneGrid`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + state: &state::Internal, + width: Length, + height: Length, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + layout_element: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + let size = limits.resolve(Size::ZERO); + + let regions = state.pane_regions(f32::from(spacing), size); + let children = elements + .filter_map(|(pane, element)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let mut node = layout_element( + element, + renderer, + &layout::Limits::new(size, size), + ); - let region = axis.split_line_bounds( - *region, - *ratio, - f32::from(self.spacing), - ); + node.move_to(Point::new(region.x, region.y)); - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match self.on_resize { - Some((leeway, _)) => { - let bounds = layout.bounds(); + Some(node) + }) + .collect(); - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); + layout::Node::with_children(size, children) +} + +/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] +/// accordingly. +pub fn update<'a, Message, T: Draggable>( + action: &mut state::Action, + state: &state::Internal, + event: &Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + spacing: u16, + elements: impl Iterator<Item = (Pane, T)>, + on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, + on_resize: &Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +) -> event::Status { + let mut event_status = event::Status::Ignored; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + event_status = event::Status::Captured; + + match on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + if action.picked_pane().is_none() { + *action = + state::Action::Resizing { split, axis }; + } + } else { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + elements, + on_click, + on_drag, + ); + } + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, _)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + let mut dropped_region = elements + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + let event = match dropped_region.next() { + Some(((target, _), _)) if pane != target => { + DragEvent::Dropped { pane, target } + } + _ => DragEvent::Canceled { pane }, + }; - let splits = self - .state - .split_regions(f32::from(self.spacing), bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - f32::from(self.spacing + leeway), - relative_cursor, - )?; - - Some(( - axis, - region + Vector::new(bounds.x, bounds.y), - false, - )) + shell.publish(on_drag(event)); } - None => None, - }); - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; + *action = state::Action::Idle; - for ((id, pane), layout) in self.elements.iter().zip(layout.children()) - { - match picked_pane { - Some((dragging, origin)) if *id == dragging => { + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + *action = state::Action::Idle; + + event_status = event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = on_resize { + if let Some((split, _)) = action.picked_split() { let bounds = layout.bounds(); - renderer.with_translation( - cursor_position - - Point::new( - bounds.x + origin.x, - bounds.y + origin.y, - ), - |renderer| { - renderer.with_layer(bounds, |renderer| { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, - ); - }); - }, - ); - } - _ => { - pane.draw( - renderer, - style, - layout, - pane_cursor_position, - viewport, + let splits = state.split_regions( + f32::from(spacing), + Size::new(bounds.width, bounds.height), ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + let ratio = match axis { + Axis::Horizontal => { + let position = + cursor_position.y - bounds.y - rectangle.y; + + (position / rectangle.height).max(0.1).min(0.9) + } + Axis::Vertical => { + let position = + cursor_position.x - bounds.x - rectangle.x; + + (position / rectangle.width).max(0.1).min(0.9) + } + }; + + shell.publish(on_resize(ResizeEvent { split, ratio })); + + event_status = event::Status::Captured; + } } } } + _ => {} + } - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - self.style_sheet.picked_split() - } else { - self.style_sheet.hovered_split() - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) - / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); + event_status +} + +fn click_pane<'a, Message, T>( + action: &mut state::Action, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + elements: 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, +{ + let mut clicked_region = elements + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().contains(cursor_position)); + + if let Some(((pane, content), 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 })); } } } +} - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.elements - .iter_mut() - .zip(layout.children()) - .filter_map(|((_, pane), layout)| pane.overlay(layout, renderer)) - .next() +/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. +pub fn mouse_interaction( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + spacing: u16, + resize_leeway: Option<u16>, +) -> Option<mouse::Interaction> { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grab); } + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let bounds = layout.bounds(); + + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + ) + .map(|(_, axis, _)| axis) + }) + }); + + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } + + None } -impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, +/// Draws a [`PaneGrid`]. +pub fn draw<Renderer, T>( + action: &state::Action, + state: &state::Internal, + layout: Layout<'_>, + cursor_position: Point, + renderer: &mut Renderer, + theme: &Renderer::Theme, + default_style: &renderer::Style, + viewport: &Rectangle, + spacing: u16, + resize_leeway: Option<u16>, + style: <Renderer::Theme as StyleSheet>::Style, + elements: impl Iterator<Item = (Pane, T)>, + draw_pane: impl Fn( + T, + &mut Renderer, + &renderer::Style, + Layout<'_>, + Point, + &Rectangle, + ), +) where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { - fn from( - pane_grid: PaneGrid<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(pane_grid) + let picked_pane = action.picked_pane(); + + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = state.split_regions(f32::from(spacing), bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = + axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = + state.split_regions(f32::from(spacing), bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + f32::from(spacing + leeway), + relative_cursor, + )?; + + Some((axis, region + Vector::new(bounds.x, bounds.y), false)) + } + None => None, + }); + + let pane_cursor_position = if picked_pane.is_some() { + // TODO: Remove once cursor availability is encoded in the type + // system + Point::new(-1.0, -1.0) + } else { + cursor_position + }; + + for ((id, pane), layout) in elements.zip(layout.children()) { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + let bounds = layout.bounds(); + + renderer.with_translation( + cursor_position + - Point::new(bounds.x + origin.x, bounds.y + origin.y), + |renderer| { + renderer.with_layer(bounds, |renderer| { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor_position, + viewport, + ); + }); + }, + ); + } + _ => { + draw_pane( + pane, + renderer, + default_style, + layout, + pane_cursor_position, + viewport, + ); + } + } + } + + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + theme.picked_split(style) + } else { + theme.hovered_split(style) + }; + + if let Some(highlight) = highlight { + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + highlight.color, + ); + } } } +/// An event produced during a drag and drop interaction of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy)] +pub enum DragEvent { + /// A [`Pane`] was picked for dragging. + Picked { + /// The picked [`Pane`]. + pane: Pane, + }, + + /// A [`Pane`] was dropped on top of another [`Pane`]. + Dropped { + /// The picked [`Pane`]. + pane: Pane, + + /// The [`Pane`] where the picked one was dropped on. + target: Pane, + }, + + /// A [`Pane`] was picked and then dropped outside of other [`Pane`] + /// boundaries. + Canceled { + /// The picked [`Pane`]. + pane: Pane, + }, +} + +/// An event produced during a resize interaction of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy)] +pub struct ResizeEvent { + /// The [`Split`] that is being dragged for resizing. + pub split: Split, + + /// The new ratio of the [`Split`]. + /// + /// The ratio is a value in [0, 1], representing the exact position of a + /// [`Split`] between two panes. + pub ratio: f32, +} + /* * Helpers */ @@ -702,8 +879,7 @@ fn hovered_split<'a>( ) -> Option<(Split, Axis, Rectangle)> { splits .filter_map(|(split, (axis, region, ratio))| { - let bounds = - axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + let bounds = axis.split_line_bounds(*region, *ratio, spacing); if bounds.contains(cursor_position) { Some((*split, *axis, bounds)) diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index 2320cb7c..02bde064 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -10,7 +10,9 @@ pub enum Axis { } impl Axis { - pub(super) fn split( + /// Splits the provided [`Rectangle`] on the current [`Axis`] with the + /// given `ratio` and `spacing`. + pub fn split( &self, rectangle: &Rectangle, ratio: f32, @@ -54,7 +56,8 @@ impl Axis { } } - pub(super) fn split_line_bounds( + /// Calculates the bounds of the split line in a [`Rectangle`] region. + pub fn split_line_bounds( &self, rectangle: Rectangle, ratio: f32, diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs index 4c52bad4..7d68fb46 100644 --- a/native/src/widget/pane_grid/configuration.rs +++ b/native/src/widget/pane_grid/configuration.rs @@ -2,7 +2,7 @@ use crate::widget::pane_grid::Axis; /// The arrangement of a [`PaneGrid`]. /// -/// [`PaneGrid`]: crate::pane_grid::PaneGrid +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone)] pub enum Configuration<T> { /// A split of the available space. @@ -21,6 +21,6 @@ pub enum Configuration<T> { }, /// A [`Pane`]. /// - /// [`Pane`]: crate::pane_grid::Pane + /// [`Pane`]: crate::widget::pane_grid::Pane Pane(T), } diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8b0e8d2a..98ce2c4b 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -4,29 +4,35 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; -use crate::widget::pane_grid::TitleBar; +use crate::widget::pane_grid::{Draggable, TitleBar}; +use crate::widget::Tree; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The content of a [`Pane`]. /// /// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] -pub struct Content<'a, Message, Renderer> { +pub struct Content<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, +{ title_bar: Option<TitleBar<'a, Message, Renderer>>, body: Element<'a, Message, Renderer>, - style_sheet: Box<dyn container::StyleSheet + 'a>, + style: <Renderer::Theme as container::StyleSheet>::Style, } impl<'a, Message, Renderer> Content<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, { /// Creates a new [`Content`] with the provided body. pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style_sheet: Default::default(), + style: Default::default(), } } @@ -42,9 +48,9 @@ where /// Sets the style of the [`Content`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } @@ -52,22 +58,52 @@ where impl<'a, Message, Renderer> Content<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, { + pub(super) fn state(&self) -> Tree { + let children = if let Some(title_bar) = self.title_bar.as_ref() { + vec![Tree::new(&self.body), title_bar.state()] + } else { + vec![Tree::new(&self.body), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(title_bar) = self.title_bar.as_ref() { + title_bar.diff(&mut tree.children[1]); + } + + tree.children[0].diff(&self.body); + } else { + *tree = self.state(); + } + } + /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::widget::pane_grid::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { + use container::StyleSheet; + let bounds = layout.bounds(); { - let style = self.style_sheet.style(); + let style = theme.appearance(self.style); container::draw_background(renderer, &style, bounds); } @@ -80,7 +116,9 @@ where let show_controls = bounds.contains(cursor_position); title_bar.draw( + &tree.children[1], renderer, + theme, style, title_bar_layout, cursor_position, @@ -88,33 +126,25 @@ where show_controls, ); - self.body.draw( + self.body.as_widget().draw( + &tree.children[0], renderer, + theme, style, body_layout, cursor_position, viewport, ); } else { - self.body - .draw(renderer, style, layout, cursor_position, viewport); - } - } - - /// Returns whether the [`Content`] with the given [`Layout`] can be picked - /// at the provided cursor position. - pub fn can_be_picked_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - title_bar.is_over_pick_area(title_bar_layout, cursor_position) - } else { - false + self.body.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); } } @@ -131,7 +161,7 @@ where let title_bar_size = title_bar_layout.size(); - let mut body_layout = self.body.layout( + let mut body_layout = self.body.as_widget().layout( renderer, &layout::Limits::new( Size::ZERO, @@ -149,12 +179,13 @@ where vec![title_bar_layout, body_layout], ) } else { - self.body.layout(renderer, limits) + self.body.as_widget().layout(renderer, limits) } } pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -169,6 +200,7 @@ where let mut children = layout.children(); event_status = title_bar.on_event( + &mut tree.children[1], event.clone(), children.next().unwrap(), cursor_position, @@ -185,7 +217,8 @@ where let body_status = if is_picked { event::Status::Ignored } else { - self.body.on_event( + self.body.as_widget_mut().on_event( + &mut tree.children[0], event, body_layout, cursor_position, @@ -200,6 +233,7 @@ where pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -218,6 +252,7 @@ where } let mouse_interaction = title_bar.mouse_interaction( + &tree.children[1], title_bar_layout, cursor_position, viewport, @@ -230,25 +265,67 @@ where }; self.body - .mouse_interaction(body_layout, cursor_position, viewport, renderer) + .as_widget() + .mouse_interaction( + &tree.children[0], + body_layout, + cursor_position, + viewport, + renderer, + ) .max(title_bar_interaction) } - pub(crate) fn overlay( - &mut self, + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - if let Some(title_bar) = self.title_bar.as_mut() { + ) -> Option<overlay::Element<'b, Message, Renderer>> { + if let Some(title_bar) = self.title_bar.as_ref() { let mut children = layout.children(); let title_bar_layout = children.next()?; - match title_bar.overlay(title_bar_layout, renderer) { + let mut states = tree.children.iter_mut(); + let body_state = states.next().unwrap(); + let title_bar_state = states.next().unwrap(); + + match title_bar.overlay(title_bar_state, title_bar_layout, renderer) + { Some(overlay) => Some(overlay), - None => self.body.overlay(children.next()?, renderer), + None => self.body.as_widget().overlay( + body_state, + children.next()?, + renderer, + ), } } else { - self.body.overlay(layout, renderer) + self.body.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } + } +} + +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, +{ + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + if let Some(title_bar) = &self.title_bar { + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + } else { + false } } } @@ -257,6 +334,7 @@ impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> where T: Into<Element<'a, Message, Renderer>>, Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, { fn from(element: T) -> Self { Self::new(element) diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs new file mode 100644 index 00000000..6044871d --- /dev/null +++ b/native/src/widget/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +use crate::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { + /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked + /// at the provided cursor position. + fn can_be_dragged_at( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool; +} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs index af6573a0..cc304b96 100644 --- a/native/src/widget/pane_grid/node.rs +++ b/native/src/widget/pane_grid/node.rs @@ -36,14 +36,11 @@ impl Node { std::iter::from_fn(move || { while let Some(node) = unvisited_nodes.pop() { - match node { - Node::Split { id, a, b, .. } => { - unvisited_nodes.push(a); - unvisited_nodes.push(b); + if let Node::Split { id, a, b, .. } = node { + unvisited_nodes.push(a); + unvisited_nodes.push(b); - return Some(id); - } - _ => {} + return Some(id); } } @@ -124,12 +121,9 @@ impl Node { } pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { - match self { - Node::Split { a, b, .. } => { - a.update(f); - b.update(f); - } - _ => {} + if let Node::Split { a, b, .. } = self { + a.update(f); + b.update(f); } f(self); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index feea0dec..cdca6267 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,3 +1,6 @@ +//! The state of a [`PaneGrid`]. +//! +//! [`PaneGrid`]: crate::widget::PaneGrid use crate::widget::pane_grid::{ Axis, Configuration, Direction, Node, Pane, Split, }; @@ -19,8 +22,15 @@ use std::collections::{BTreeMap, HashMap}; /// [`PaneGrid::new`]: crate::widget::PaneGrid::new #[derive(Debug, Clone)] pub struct State<T> { - pub(super) panes: HashMap<Pane, T>, - pub(super) internal: Internal, + /// The panes of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub panes: HashMap<Pane, T>, + + /// The internal state of the [`PaneGrid`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub internal: Internal, } impl<T> State<T> { @@ -39,17 +49,10 @@ impl<T> State<T> { pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { let mut panes = HashMap::new(); - let (layout, last_id) = - Self::distribute_content(&mut panes, config.into(), 0); + let internal = + Internal::from_configuration(&mut panes, config.into(), 0); - State { - panes, - internal: Internal { - layout, - last_id, - action: Action::Idle, - }, - } + State { panes, internal } } /// Returns the total amount of panes in the [`State`]. @@ -57,6 +60,11 @@ impl<T> State<T> { self.panes.len() } + /// Returns `true` if the amount of panes in the [`State`] is 0. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns the internal state of the given [`Pane`], if it exists. pub fn get(&self, pane: &Pane) -> Option<&T> { self.panes.get(pane) @@ -192,16 +200,38 @@ impl<T> State<T> { None } } +} + +/// The internal state of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid +#[derive(Debug, Clone)] +pub struct Internal { + layout: Node, + last_id: usize, +} - fn distribute_content( +impl Internal { + /// Initializes the [`Internal`] state of a [`PaneGrid`] from a + /// [`Configuration`]. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + pub fn from_configuration<T>( panes: &mut HashMap<Pane, T>, content: Configuration<T>, next_id: usize, - ) -> (Node, usize) { - match content { + ) -> Self { + let (layout, last_id) = match content { Configuration::Split { axis, ratio, a, b } => { - let (a, next_id) = Self::distribute_content(panes, *a, next_id); - let (b, next_id) = Self::distribute_content(panes, *b, next_id); + let Internal { + layout: a, + last_id: next_id, + } = Self::from_configuration(panes, *a, next_id); + + let Internal { + layout: b, + last_id: next_id, + } = Self::from_configuration(panes, *b, next_id); ( Node::Split { @@ -220,39 +250,63 @@ impl<T> State<T> { (Node::Pane(id), next_id + 1) } - } - } -} + }; -#[derive(Debug, Clone)] -pub struct Internal { - layout: Node, - last_id: usize, - action: Action, + Self { layout, last_id } + } } +/// The current action of a [`PaneGrid`]. +/// +/// [`PaneGrid`]: crate::widget::PaneGrid #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { + /// The [`PaneGrid`] is idle. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid Idle, - Dragging { pane: Pane, origin: Point }, - Resizing { split: Split, axis: Axis }, + /// A [`Pane`] in the [`PaneGrid`] is being dragged. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + Dragging { + /// The [`Pane`] being dragged. + pane: Pane, + /// The starting [`Point`] of the drag interaction. + origin: Point, + }, + /// A [`Split`] in the [`PaneGrid`] is being dragged. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid + Resizing { + /// The [`Split`] being dragged. + split: Split, + /// The [`Axis`] of the [`Split`]. + axis: Axis, + }, } -impl Internal { +impl Action { + /// Returns the current [`Pane`] that is being dragged, if any. pub fn picked_pane(&self) -> Option<(Pane, Point)> { - match self.action { + match *self { Action::Dragging { 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.action { + match *self { Action::Resizing { split, axis, .. } => Some((split, axis)), _ => None, } } +} +impl Internal { + /// Calculates the current [`Pane`] regions from the [`PaneGrid`] layout. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid pub fn pane_regions( &self, spacing: f32, @@ -261,6 +315,9 @@ impl Internal { self.layout.pane_regions(spacing, size) } + /// Calculates the current [`Split`] regions from the [`PaneGrid`] layout. + /// + /// [`PaneGrid`]: crate::widget::PaneGrid pub fn split_regions( &self, spacing: f32, @@ -268,28 +325,4 @@ impl Internal { ) -> BTreeMap<Split, (Axis, Rectangle, f32)> { self.layout.split_regions(spacing, size) } - - pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { - self.action = Action::Dragging { - pane: *pane, - origin, - }; - } - - pub fn pick_split(&mut self, split: &Split, axis: Axis) { - // TODO: Obtain `axis` from layout itself. Maybe we should implement - // `Node::find_split` - if self.picked_pane().is_some() { - return; - } - - self.action = Action::Resizing { - split: *split, - axis, - }; - } - - pub fn idle(&mut self) { - self.action = Action::Idle; - } } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index d56972ec..eb85f924 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -4,6 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget::container; +use crate::widget::Tree; use crate::{ Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, }; @@ -12,17 +13,22 @@ use crate::{ /// /// [`Pane`]: crate::widget::pane_grid::Pane #[allow(missing_debug_implementations)] -pub struct TitleBar<'a, Message, Renderer> { +pub struct TitleBar<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, +{ content: Element<'a, Message, Renderer>, controls: Option<Element<'a, Message, Renderer>>, padding: Padding, always_show_controls: bool, - style_sheet: Box<dyn container::StyleSheet + 'a>, + style: <Renderer::Theme as container::StyleSheet>::Style, } impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, { /// Creates a new [`TitleBar`] with the given content. pub fn new<E>(content: E) -> Self @@ -34,7 +40,7 @@ where controls: None, padding: Padding::ZERO, always_show_controls: false, - style_sheet: Default::default(), + style: Default::default(), } } @@ -56,9 +62,9 @@ where /// Sets the style of the [`TitleBar`]. pub fn style( mut self, - style: impl Into<Box<dyn container::StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, ) -> Self { - self.style_sheet = style.into(); + self.style = style.into(); self } @@ -79,21 +85,51 @@ where impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> where Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, { + pub(super) fn state(&self) -> Tree { + let children = if let Some(controls) = self.controls.as_ref() { + vec![Tree::new(&self.content), Tree::new(controls)] + } else { + vec![Tree::new(&self.content), Tree::empty()] + }; + + Tree { + children, + ..Tree::empty() + } + } + + pub(super) fn diff(&self, tree: &mut Tree) { + if tree.children.len() == 2 { + if let Some(controls) = self.controls.as_ref() { + tree.children[1].diff(controls); + } + + tree.children[0].diff(&self.content); + } else { + *tree = self.state(); + } + } + /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::widget::pane_grid::Renderer + /// [`Renderer`]: iced_native::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, show_controls: bool, ) { + use container::StyleSheet; + let bounds = layout.bounds(); - let style = self.style_sheet.style(); + let style = theme.appearance(self.style); let inherited_style = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), }; @@ -105,21 +141,21 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - - self.content.draw( - renderer, - &inherited_style, - title_layout, - cursor_position, - viewport, - ); + let mut show_title = true; if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - if show_controls || self.always_show_controls { - controls.draw( + let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } + + controls.as_widget().draw( + &tree.children[1], renderer, + theme, &inherited_style, controls_layout, cursor_position, @@ -127,6 +163,18 @@ where ); } } + + if show_title { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &inherited_style, + title_layout, + cursor_position, + viewport, + ); + } } /// Returns whether the mouse cursor is over the pick area of the @@ -147,8 +195,14 @@ where if self.controls.is_some() { let controls_layout = children.next().unwrap(); - !controls_layout.bounds().contains(cursor_position) - && !title_layout.bounds().contains(cursor_position) + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + !controls_layout.bounds().contains(cursor_position) + } else { + !controls_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } } else { !title_layout.bounds().contains(cursor_position) } @@ -167,11 +221,14 @@ where let title_layout = self .content + .as_widget() .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); + let title_size = title_layout.size(); let mut node = if let Some(controls) = &self.controls { let mut controls_layout = controls + .as_widget() .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); let controls_size = controls_layout.size(); @@ -202,6 +259,7 @@ where pub(crate) fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -214,11 +272,18 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); + let mut show_title = true; let control_status = if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + show_title = false; + } - controls.on_event( + controls.as_widget_mut().on_event( + &mut tree.children[1], event.clone(), controls_layout, cursor_position, @@ -230,20 +295,26 @@ where event::Status::Ignored }; - let title_status = self.content.on_event( - event, - title_layout, - cursor_position, - renderer, - clipboard, - shell, - ); + let title_status = if show_title { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + title_layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } else { + event::Status::Ignored + }; control_status.merge(title_status) } pub(crate) fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -255,7 +326,8 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - let title_interaction = self.content.mouse_interaction( + let title_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], title_layout, cursor_position, viewport, @@ -264,25 +336,32 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); + let controls_interaction = controls.as_widget().mouse_interaction( + &tree.children[1], + controls_layout, + cursor_position, + viewport, + renderer, + ); - controls - .mouse_interaction( - controls_layout, - cursor_position, - viewport, - renderer, - ) - .max(title_interaction) + if title_layout.bounds().width + controls_layout.bounds().width + > padded.bounds().width + { + controls_interaction + } else { + controls_interaction.max(title_interaction) + } } else { title_interaction } } - pub(crate) fn overlay( - &mut self, + pub(crate) fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { + ) -> Option<overlay::Element<'b, Message, Renderer>> { let mut children = layout.children(); let padded = children.next()?; @@ -293,12 +372,23 @@ where content, controls, .. } = self; - content.overlay(title_layout, renderer).or_else(move || { - controls.as_mut().and_then(|controls| { - let controls_layout = children.next()?; - - controls.overlay(controls_layout, renderer) + let mut states = tree.children.iter_mut(); + let title_state = states.next().unwrap(); + let controls_state = states.next().unwrap(); + + content + .as_widget() + .overlay(title_state, title_layout, renderer) + .or_else(move || { + controls.as_ref().and_then(|controls| { + let controls_layout = children.next()?; + + controls.as_widget().overlay( + controls_state, + controls_layout, + renderer, + ) + }) }) - }) } } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 3be6c20c..c334804e 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -9,26 +9,24 @@ use crate::overlay::menu::{self, Menu}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget::tree::{self, Tree}; use crate::{ Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Widget, }; use std::borrow::Cow; -pub use iced_style::pick_list::{Style, StyleSheet}; +pub use iced_style::pick_list::{Appearance, StyleSheet}; /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] -pub struct PickList<'a, T, Message, Renderer: text::Renderer> +pub struct PickList<'a, T, Message, Renderer> where [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { - menu: &'a mut menu::State, - keyboard_modifiers: &'a mut keyboard::Modifiers, - is_open: &'a mut bool, - hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, - on_selected: Box<dyn Fn(T) -> Message>, + on_selected: Box<dyn Fn(T) -> Message + 'a>, options: Cow<'a, [T]>, placeholder: Option<String>, selected: Option<T>, @@ -36,63 +34,27 @@ where padding: Padding, text_size: Option<u16>, font: Renderer::Font, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -/// The local state of a [`PickList`]. -#[derive(Debug, Clone)] -pub struct State<T> { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option<usize>, - last_selection: Option<T>, -} - -impl<T> Default for State<T> { - fn default() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl<'a, T: 'a, Message, Renderer: text::Renderer> - PickList<'a, T, Message, Renderer> +impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> where T: ToString + Eq, [T]: ToOwned<Owned = Vec<T>>, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { /// The default padding of a [`PickList`]. pub const DEFAULT_PADDING: Padding = Padding::new(5); - /// Creates a new [`PickList`] with the given [`State`], a list of options, - /// the current selected value, and the message to produce when an option is - /// selected. + /// Creates a new [`PickList`] with the given list of options, the current + /// selected value, and the message to produce when an option is selected. pub fn new( - state: &'a mut State<T>, options: impl Into<Cow<'a, [T]>>, selected: Option<T>, - on_selected: impl Fn(T) -> Message + 'static, + on_selected: impl Fn(T) -> Message + 'a, ) -> Self { - let State { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, - } = state; - Self { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -101,7 +63,7 @@ where text_size: None, padding: Self::DEFAULT_PADDING, font: Default::default(), - style_sheet: Default::default(), + style: Default::default(), } } @@ -138,9 +100,9 @@ where /// Sets the style of the [`PickList`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } @@ -148,11 +110,20 @@ where impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for PickList<'a, T, Message, Renderer> where - T: Clone + ToString + Eq, + T: Clone + ToString + Eq + 'static, [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, + Message: 'a, Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<T>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::<T>::new()) + } + fn width(&self) -> Length { self.width } @@ -166,62 +137,21 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - use std::f32; - - let limits = limits - .width(self.width) - .height(Length::Shrink) - .pad(self.padding); - - let text_size = self.text_size.unwrap_or(renderer.default_size()); - let font = self.font.clone(); - - let max_width = match self.width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let (width, _) = renderer.measure( - label, - text_size, - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }; - - let labels = self.options.iter().map(ToString::to_string); - - let labels_width = - labels.map(|label| measure(&label)).max().unwrap_or(100); - - let placeholder_width = self - .placeholder - .as_ref() - .map(String::as_str) - .map(measure) - .unwrap_or(100); - - labels_width.max(placeholder_width) - } - _ => 0, - }; - - let size = { - let intrinsic = Size::new( - max_width as f32 - + f32::from(text_size) - + f32::from(self.padding.left), - f32::from(text_size), - ); - - limits.resolve(intrinsic).pad(self.padding) - }; - - layout::Node::new(size) + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_deref(), + &self.options, + ) } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -229,44 +159,228 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let event_status = if *self.is_open { - // TODO: Encode cursor availability in the type system - *self.is_open = - cursor_position.x < 0.0 || cursor_position.y < 0.0; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - let selected = self.selected.as_ref(); - - *self.is_open = true; - *self.hovered_option = self - .options - .iter() - .position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || tree.state.downcast_mut::<State<T>>(), + ) + } - if let Some(last_selection) = self.last_selection.take() { - shell.publish((self.on_selected)(last_selection)); + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } - *self.is_open = false; + fn draw( + &self, + _tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_deref(), + self.selected.as_ref(), + self.style, + ) + } - event::Status::Captured - } else { - event_status - } + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + let state = tree.state.downcast_mut::<State<T>>(); + + overlay( + layout, + state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style, + ) + } +} + +impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> + for Element<'a, Message, Renderer> +where + T: Clone + ToString + Eq + 'static, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { + Self::new(pick_list) + } +} + +/// The local state of a [`PickList`]. +#[derive(Debug)] +pub struct State<T> { + menu: menu::State, + keyboard_modifiers: keyboard::Modifiers, + is_open: bool, + hovered_option: Option<usize>, + last_selection: Option<T>, +} + +impl<T> State<T> { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { + Self { + menu: menu::State::default(), + keyboard_modifiers: keyboard::Modifiers::default(), + is_open: bool::default(), + hovered_option: Option::default(), + last_selection: Option::default(), + } + } +} + +impl<T> Default for State<T> { + fn default() -> Self { + Self::new() + } +} + +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node +where + Renderer: text::Renderer, + T: ToString, +{ + use std::f32; + + let limits = limits.width(width).height(Length::Shrink).pad(padding); + + let text_size = text_size.unwrap_or_else(|| renderer.default_size()); + + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font.clone(), + Size::new(f32::INFINITY, f32::INFINITY), + ); + + width.round() as u32 + }; + + let labels = options.iter().map(ToString::to_string); + + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); + + let placeholder_width = placeholder.map(measure).unwrap_or(100); + + labels_width.max(placeholder_width) + } + _ => 0, + }; + + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size) + f32::from(padding.left), + f32::from(text_size), + ); + + limits.resolve(intrinsic).pad(padding) + }; + + layout::Node::new(size) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State<T>, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + + let event_status = if state.is_open { + // TODO: Encode cursor availability in the type system + state.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; + + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) if self.keyboard_modifiers.command() + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() && layout.bounds().contains(cursor_position) - && !*self.is_open => + && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, @@ -278,162 +392,170 @@ where } let next_option = if y < 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter()) + if let Some(selected) = selected { + find_next(selected, options.iter()) } else { - self.options.first() + options.first() } } else if y > 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter().rev()) + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) } else { - self.options.last() + options.last() } } else { None }; if let Some(next_option) = next_option { - shell.publish((self.on_selected)(next_option.clone())); + shell.publish((on_selected)(next_option.clone())); } event::Status::Captured - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - *self.keyboard_modifiers = modifiers; - + } else { event::Status::Ignored } - _ => event::Status::Ignored, } - } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + state.keyboard_modifiers = modifiers; - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() + event::Status::Ignored } + _ => event::Status::Ignored, } +} - fn draw( - &self, - renderer: &mut Renderer, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = self.selected.is_some(); - - let style = if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius, - }, - style.background, - ); - - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - - f32::from(self.padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - - if let Some(label) = self - .selected - .as_ref() - .map(ToString::to_string) - .as_ref() - .or_else(|| self.placeholder.as_ref()) - { - renderer.fill_text(Text { - content: label, - size: f32::from( - self.text_size.unwrap_or(renderer.default_size()), - ), - font: self.font.clone(), - color: is_selected - .then(|| style.text_color) - .unwrap_or(style.placeholder_color), - bounds: Rectangle { - x: bounds.x + f32::from(self.padding.left), - y: bounds.center_y(), - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }) - } +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() } +} - fn overlay( - &mut self, - layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - if *self.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut self.menu, - &self.options, - &mut self.hovered_option, - &mut self.last_selection, - ) - .width(bounds.width.round() as u16) - .padding(self.padding) - .font(self.font.clone()) - .style(self.style_sheet.menu()); - - if let Some(text_size) = self.text_size { - menu = menu.text_size(text_size); - } +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State<T>, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + options: &'a [T], + style: <Renderer::Theme as StyleSheet>::Style, +) -> Option<overlay::Element<'a, Message, Renderer>> +where + T: Clone + ToString, + Message: 'a, + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + if state.is_open { + let bounds = layout.bounds(); - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None + let mut menu = Menu::new( + &mut state.menu, + options, + &mut state.hovered_option, + &mut state.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(padding) + .font(font) + .style(style); + + if let Some(text_size) = text_size { + menu = menu.text_size(text_size); } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None } } -impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> - for PickList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Renderer: text::Renderer + 'a, - Message: 'static, +/// Draws a [`PickList`]. +pub fn draw<T, Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor_position: Point, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + style: <Renderer::Theme as StyleSheet>::Style, +) where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, + T: ToString, { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let is_selected = selected.is_some(); + + let style = if is_mouse_over { + theme.hovered(style) + } else { + theme.active(style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + renderer.fill_text(Text { + content: &Renderer::ARROW_DOWN_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding.horizontal()), + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + + let label = selected.map(ToString::to_string); + + if let Some(label) = label.as_deref().or(placeholder) { + let text_size = + f32::from(text_size.unwrap_or_else(|| renderer.default_size())); + + renderer.fill_text(Text { + content: label, + size: text_size, + font: font.clone(), + color: if is_selected { + style.text_color + } else { + style.placeholder_color + }, + bounds: Rectangle { + x: bounds.x + f32::from(padding.left), + y: bounds.center_y() - text_size / 2.0, + width: bounds.width - f32::from(padding.horizontal()), + height: text_size, + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }); } } diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index c26c38fa..8a945433 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -1,17 +1,18 @@ //! Provide progress feedback to your users. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; use std::ops::RangeInclusive; -pub use iced_style::progress_bar::{Style, StyleSheet}; +pub use iced_style::progress_bar::{Appearance, StyleSheet}; /// A bar that displays progress. /// /// # Example /// ``` -/// # use iced_native::widget::ProgressBar; +/// # type ProgressBar = iced_native::widget::ProgressBar<iced_native::renderer::Null>; /// let value = 50.0; /// /// ProgressBar::new(0.0..=100.0, value); @@ -19,15 +20,23 @@ pub use iced_style::progress_bar::{Style, StyleSheet}; /// ///  #[allow(missing_debug_implementations)] -pub struct ProgressBar<'a> { +pub struct ProgressBar<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a> ProgressBar<'a> { +impl<Renderer> ProgressBar<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: u16 = 30; @@ -42,7 +51,7 @@ impl<'a> ProgressBar<'a> { range, width: Length::Fill, height: None, - style_sheet: Default::default(), + style: Default::default(), } } @@ -61,16 +70,17 @@ impl<'a> ProgressBar<'a> { /// Sets the style of the [`ProgressBar`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for ProgressBar<'a> +impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { fn width(&self) -> Length { self.width @@ -96,7 +106,9 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -112,7 +124,7 @@ where / (range_end - range_start) }; - let style = self.style_sheet.style(); + let style = theme.appearance(self.style); renderer.fill_quad( renderer::Quad { @@ -141,13 +153,16 @@ where } } -impl<'a, Message, Renderer> From<ProgressBar<'a>> +impl<'a, Message, Renderer> From<ProgressBar<Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, { - fn from(progress_bar: ProgressBar<'a>) -> Element<'a, Message, Renderer> { + fn from( + progress_bar: ProgressBar<Renderer>, + ) -> Element<'a, Message, Renderer> { Element::new(progress_bar) } } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index fed2925b..c9152d05 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -6,20 +6,20 @@ use crate::mouse; use crate::renderer; use crate::text; use crate::touch; -use crate::widget::{self, Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Widget, }; -pub use iced_style::radio::{Style, StyleSheet}; +pub use iced_style::radio::{Appearance, StyleSheet}; /// A circular button representing a choice. /// /// # Example /// ``` -/// # type Radio<'a, Message> = -/// # iced_native::widget::Radio<'a, Message, iced_native::renderer::Null>; +/// # type Radio<Message> = +/// # iced_native::widget::Radio<Message, iced_native::renderer::Null>; /// # /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// pub enum Choice { @@ -41,7 +41,11 @@ pub use iced_style::radio::{Style, StyleSheet}; /// ///  #[allow(missing_debug_implementations)] -pub struct Radio<'a, Message, Renderer: text::Renderer> { +pub struct Radio<Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ is_selected: bool, on_click: Message, label: String, @@ -50,12 +54,14 @@ pub struct Radio<'a, Message, Renderer: text::Renderer> { spacing: u16, text_size: Option<u16>, font: Renderer::Font, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, Message, Renderer: text::Renderer> Radio<'a, Message, Renderer> +impl<Message, Renderer> Radio<Message, Renderer> where Message: Clone, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: u16 = 28; @@ -79,7 +85,7 @@ where ) -> Self where V: Eq + Copy, - F: 'static + Fn(V) -> Message, + F: FnOnce(V) -> Message, { Radio { is_selected: Some(value) == selected, @@ -90,7 +96,7 @@ where spacing: Self::DEFAULT_SPACING, //15 text_size: None, font: Default::default(), - style_sheet: Default::default(), + style: Default::default(), } } @@ -127,18 +133,18 @@ where /// Sets the style of the [`Radio`] button. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Radio<'a, Message, Renderer> +impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer> where Message: Clone, Renderer: text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { fn width(&self) -> Length { self.width @@ -162,16 +168,15 @@ where .width(Length::Units(self.size)) .height(Length::Units(self.size)), ) - .push( - Text::new(&self.label) - .width(self.width) - .size(self.text_size.unwrap_or(renderer.default_size())), - ) + .push(Text::new(&self.label).width(self.width).size( + self.text_size.unwrap_or_else(|| renderer.default_size()), + )) .layout(renderer, limits) } fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -196,6 +201,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -210,7 +216,9 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -222,9 +230,9 @@ where let mut children = layout.children(); let custom_style = if is_mouse_over { - self.style_sheet.hovered() + theme.hovered(self.style) } else { - self.style_sheet.active() + theme.active(self.style) }; { @@ -270,9 +278,11 @@ where style, label_layout, &self.label, - self.font.clone(), self.text_size, - custom_style.text_color, + self.font.clone(), + widget::text::Appearance { + color: custom_style.text_color, + }, alignment::Horizontal::Left, alignment::Vertical::Center, ); @@ -280,15 +290,14 @@ where } } -impl<'a, Message, Renderer> From<Radio<'a, Message, Renderer>> +impl<'a, Message, Renderer> From<Radio<Message, Renderer>> for Element<'a, Message, Renderer> where Message: 'a + Clone, Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { - fn from( - radio: Radio<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { + fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> { Element::new(radio) } } diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index e34befb2..eda7c2d3 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,16 +1,15 @@ //! Distribute content horizontally. use crate::event::{self, Event}; -use crate::layout; +use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; +use crate::widget::{Operation, Tree}; use crate::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Widget, + Alignment, Clipboard, Element, Length, Padding, Point, Rectangle, Shell, + Widget, }; -use std::u32; - /// A container that distributes its contents horizontally. #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Renderer> { @@ -18,8 +17,6 @@ pub struct Row<'a, Message, Renderer> { padding: Padding, width: Length, height: Length, - max_width: u32, - max_height: u32, align_items: Alignment, children: Vec<Element<'a, Message, Renderer>>, } @@ -39,8 +36,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - max_width: u32::MAX, - max_height: u32::MAX, align_items: Alignment::Start, children, } @@ -48,7 +43,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { /// Sets the horizontal spacing _between_ elements. /// - /// Custom margins per element do not exist in Iced. You should use this + /// Custom margins per element do not exist in iced. You should use this /// method instead! While less flexible, it helps you keep spacing between /// elements consistent. pub fn spacing(mut self, units: u16) -> Self { @@ -74,18 +69,6 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { self } - /// Sets the maximum width of the [`Row`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - - /// Sets the maximum height of the [`Row`]. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the vertical alignment of the contents of the [`Row`] . pub fn align_items(mut self, align: Alignment) -> Self { self.align_items = align; @@ -93,20 +76,34 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { } /// Adds an [`Element`] to the [`Row`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { self.children.push(child.into()); self } } +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { + fn default() -> Self { + Self::new() + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Row<'a, Message, Renderer> where Renderer: crate::Renderer, { + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children) + } + fn width(&self) -> Length { self.width } @@ -120,11 +117,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height); + let limits = limits.width(self.width).height(self.height); layout::flex::resolve( layout::flex::Axis::Horizontal, @@ -137,8 +130,26 @@ where ) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child.as_widget().operate(state, layout, operation); + }) + }); + } + fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -148,9 +159,11 @@ where ) -> event::Status { self.children .iter_mut() + .zip(&mut tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.on_event( + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, event.clone(), layout, cursor_position, @@ -164,6 +177,7 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -171,9 +185,11 @@ where ) -> mouse::Interaction { self.children .iter() + .zip(&tree.children) .zip(layout.children()) - .map(|(child, layout)| { - child.widget.mouse_interaction( + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor_position, viewport, @@ -186,39 +202,49 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, style, layout, cursor_position, 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_position, + viewport, + ); } } - fn overlay( - &mut self, + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - self.children - .iter_mut() - .zip(layout.children()) - .filter_map(|(child, layout)| { - child.widget.overlay(layout, renderer) - }) - .next() + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&self.children, tree, layout, renderer) } } impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: crate::Renderer + 'a, { - fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { - Element::new(row) + fn from(row: Row<'a, Message, Renderer>) -> Self { + Self::new(row) } } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index b0cc3768..56f8c80d 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -1,53 +1,63 @@ //! Display a horizontal or vertical rule for dividing content. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; -pub use iced_style::rule::{FillMode, Style, StyleSheet}; +pub use iced_style::rule::{Appearance, FillMode, StyleSheet}; /// Display a horizontal or vertical rule for dividing content. #[allow(missing_debug_implementations)] -pub struct Rule<'a> { +pub struct Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ width: Length, height: Length, is_horizontal: bool, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a> Rule<'a> { - /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. - pub fn horizontal(spacing: u16) -> Self { +impl<Renderer> Rule<Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a horizontal [`Rule`] with the given height. + pub fn horizontal(height: u16) -> Self { Rule { width: Length::Fill, - height: Length::from(Length::Units(spacing)), + height: Length::Units(height), is_horizontal: true, - style_sheet: Default::default(), + style: Default::default(), } } - /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. - pub fn vertical(spacing: u16) -> Self { + /// Creates a vertical [`Rule`] with the given width. + pub fn vertical(width: u16) -> Self { Rule { - width: Length::from(Length::Units(spacing)), + width: Length::Units(width), height: Length::Fill, is_horizontal: false, - style_sheet: Default::default(), + style: Default::default(), } } /// Sets the style of the [`Rule`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Rule<'a> +impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer> where Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { fn width(&self) -> Length { self.width @@ -69,14 +79,16 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let style = self.style_sheet.style(); + let style = theme.style(self.style); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) @@ -120,12 +132,14 @@ where } } -impl<'a, Message, Renderer> From<Rule<'a>> for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From<Rule<Renderer>> + for Element<'a, Message, Renderer> where - Renderer: 'a + crate::Renderer, Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, { - fn from(rule: Rule<'a>) -> Element<'a, Message, Renderer> { + fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> { Element::new(rule) } } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ce734ad8..4ebb07a0 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -5,66 +5,65 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::touch; -use crate::widget::Column; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Alignment, Background, Clipboard, Color, Element, Layout, Length, Padding, - Point, Rectangle, Shell, Size, Vector, Widget, + Background, Clipboard, Color, Command, Element, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::{f32, u32}; pub use iced_style::scrollable::StyleSheet; +pub mod style { + //! The styles of a [`Scrollable`]. + //! + //! [`Scrollable`]: crate::widget::Scrollable + pub use iced_style::scrollable::{Scrollbar, Scroller}; +} + /// A widget that can vertically display an infinite amount of content with a /// scrollbar. #[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer> { - state: &'a mut State, +pub struct Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option<Id>, height: Length, - max_height: u32, scrollbar_width: u16, scrollbar_margin: u16, scroller_width: u16, - content: Column<'a, Message, Renderer>, - on_scroll: Option<Box<dyn Fn(f32) -> Message>>, - style_sheet: Box<dyn StyleSheet + 'a>, + content: Element<'a, Message, Renderer>, + on_scroll: Option<Box<dyn Fn(f32) -> Message + 'a>>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { - /// Creates a new [`Scrollable`] with the given [`State`]. - pub fn new(state: &'a mut State) -> Self { +impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Scrollable`]. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Scrollable { - state, + id: None, height: Length::Shrink, - max_height: u32::MAX, scrollbar_width: 10, scrollbar_margin: 0, scroller_width: 10, - content: Column::new(), + content: content.into(), on_scroll: None, - style_sheet: Default::default(), + style: Default::default(), } } - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in Iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, units: u16) -> Self { - self.content = self.content.spacing(units); - self - } - - /// Sets the [`Padding`] of the [`Scrollable`]. - pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { - self.content = self.content.padding(padding); - self - } - - /// Sets the width of the [`Scrollable`]. - pub fn width(mut self, width: Length) -> Self { - self.content = self.content.width(width); + /// Sets the [`Id`] of the [`Scrollable`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); self } @@ -74,24 +73,6 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { self } - /// Sets the maximum width of the [`Scrollable`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.content = self.content.max_width(max_width); - self - } - - /// Sets the maximum height of the [`Scrollable`] in pixels. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - - /// Sets the horizontal alignment of the contents of the [`Scrollable`] . - pub fn align_items(mut self, align_items: Alignment) -> Self { - self.content = self.content.align_items(align_items); - self - } - /// Sets the scrollbar width of the [`Scrollable`] . /// Silently enforces a minimum value of 1. pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { @@ -117,7 +98,7 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { /// /// The function takes the new relative offset of the [`Scrollable`] /// (e.g. `0` means top, while `1` means bottom). - pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self { + pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -125,97 +106,37 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { /// Sets the style of the [`Scrollable`] . pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } +} - /// Adds an element to the [`Scrollable`]. - pub fn push<E>(mut self, child: E) -> Self - where - E: Into<Element<'a, Message, Renderer>>, - { - self.content = self.content.push(child); - self +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() } - fn notify_on_scroll( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - shell: &mut Shell<'_, Message>, - ) { - if content_bounds.height <= bounds.height { - return; - } + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } - if let Some(on_scroll) = &self.on_scroll { - shell.publish(on_scroll( - self.state.offset.absolute(bounds, content_bounds) - / (content_bounds.height - bounds.height), - )); - } + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] } - fn scrollbar( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Option<Scrollbar> { - let offset = self.state.offset(bounds, content_bounds); - - if content_bounds.height > bounds.height { - let outer_width = self.scrollbar_width.max(self.scroller_width) - + 2 * self.scrollbar_margin; - - let outer_bounds = Rectangle { - x: bounds.x + bounds.width - outer_width as f32, - y: bounds.y, - width: outer_width as f32, - height: bounds.height, - }; - - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scrollbar_width / 2), - y: bounds.y, - width: self.scrollbar_width as f32, - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scroller_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scroller_width / 2), - y: scrollbar_bounds.y + y_offset, - width: self.scroller_width as f32, - height: scroller_height, - }; - - Some(Scrollbar { - outer_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) } -} -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ fn width(&self) -> Length { - Widget::<Message, Renderer>::width(&self.content) + self.content.as_widget().width() } fn height(&self) -> Length { @@ -227,24 +148,40 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_height(self.max_height) - .width(Widget::<Message, Renderer>::width(&self.content)) - .height(self.height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), - Size::new(limits.max().width, f32::INFINITY), - ); + layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + u32::MAX, + |renderer, limits| { + self.content.as_widget().layout(renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); - let content = self.content.layout(renderer, &child_limits); - let size = limits.resolve(content.size()); + operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); - layout::Node::with_children(size, vec![content]) + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + operation, + ); + }); } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -252,122 +189,351 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbar = self.scrollbar(bounds, content_bounds); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, + update( + tree.state.downcast_mut::<State>(), + event, + layout, + cursor_position, + clipboard, + shell, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event.clone(), - content, - cursor_position, + }, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + tree.state.downcast_ref::<State>(), + renderer, + theme, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style, + |renderer, layout, cursor_position, viewport| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + }, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + tree.state.downcast_ref::<State>(), + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) + } + + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content + .as_widget() + .overlay( + &mut tree.children[0], + layout.children().next().unwrap(), renderer, - clipboard, - shell, ) + .map(|overlay| { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = tree + .state + .downcast_ref::<State>() + .offset(bounds, content_bounds); + + overlay.translate(Vector::new(0.0, -(offset as f32))) + }) + } +} + +impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: Scrollable<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`Scrollable`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided `percentage`. +pub fn snap_to<Message: 'static>(id: Id, percentage: f32) -> Command<Message> { + Command::widget(operation::scrollable::snap_to(id.0, percentage)) +} + +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + max_height: u32, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.max_height(max_height).width(width).height(height); + + let child_limits = layout::Limits::new( + Size::new(limits.min().width, 0.0), + Size::new(limits.max().width, f32::INFINITY), + ); + + let content = layout_content(renderer, &child_limits); + let size = limits.resolve(content.size()); + + layout::Node::with_children(size, vec![content]) +} + +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update<Message>( + state: &mut State, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + Point, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + state.offset(bounds, content_bounds) as f32, + ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) }; - if let event::Status::Captured = event_status { - return event::Status::Captured; - } + update_content( + event.clone(), + content, + cursor_position, + clipboard, + shell, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if is_mouse_over { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + // TODO: Configurable speed (?) + state.scroll(y * 60.0, bounds, content_bounds); + } + mouse::ScrollDelta::Pixels { y, .. } => { + state.scroll(y, bounds, content_bounds); + } + } - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - self.state.scroll(y * 60.0, bounds, content_bounds); - } - mouse::ScrollDelta::Pixels { y, .. } => { - self.state.scroll(y, bounds, content_bounds); - } + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + state.scroll_box_touched_at = Some(cursor_position); } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroll(delta, bounds, content_bounds); - return event::Status::Captured; - } - Event::Touch(event) => { - match event { - touch::Event::FingerPressed { .. } => { - self.state.scroll_box_touched_at = - Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - self.state.scroll_box_touched_at - { - let delta = - cursor_position.y - scroll_box_touched_at.y; - - self.state.scroll( - delta, - bounds, - content_bounds, - ); - - self.state.scroll_box_touched_at = - Some(cursor_position); - - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - self.state.scroll_box_touched_at = None; + state.scroll_box_touched_at = Some(cursor_position); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_box_touched_at = None; + } } - _ => {} + + return event::Status::Captured; } + _ => {} } + } - if self.state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.scroller_grabbed_at = None; + if state.is_scroller_grabbed() { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.scroller_grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, state.scroller_grabbed_at) + { + state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, self.state.scroller_grabbed_at) + } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) { - self.state.scroll_to( + state.scroll_to( scrollbar.scroll_percentage( scroller_grabbed_at, cursor_position, @@ -376,112 +542,63 @@ where content_bounds, ); - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroller_grabbed_at = Some(scroller_grabbed_at); - return event::Status::Captured; - } - } - _ => {} - } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - self.state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - self.state.scroller_grabbed_at = - Some(scroller_grabbed_at); - - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); - return event::Status::Captured; - } + return event::Status::Captured; } } - _ => {} } + _ => {} } - - event::Status::Ignored } - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() { - mouse::Interaction::Idle - } else { - let offset = self.state.offset(bounds, content_bounds); - - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) - } else { - Point::new(cursor_position.x, -1.0) - }; - - self.content.mouse_interaction( - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - renderer, - ) - } - } + event::Status::Ignored +} - fn draw( - &self, - renderer: &mut Renderer, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( + state: &State, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + content_interaction: impl FnOnce( + Layout<'_>, + Point, + &Rectangle, + ) -> mouse::Interaction, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + let offset = state.offset(bounds, content_bounds); let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { Point::new(cursor_position.x, cursor_position.y + offset as f32) @@ -489,104 +606,206 @@ where Point::new(cursor_position.x, -1.0) }; - if let Some(scrollbar) = scrollbar { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(0.0, -(offset as f32)), - |renderer| { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - }, - ); - }); - - let style = if self.state.is_scroller_grabbed() { - self.style_sheet.dragging() - } else if is_mouse_over_scrollbar { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; + content_interaction( + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ) + } +} - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( + state: &State, + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + style: <Renderer::Theme as StyleSheet>::Style, + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), +) where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) + }; + + if let Some(scrollbar) = scrollbar { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(0.0, -(offset as f32)), |renderer| { - if is_scrollbar_visible { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background.unwrap_or(Background::Color( - Color::TRANSPARENT, - )), - ); - } - - if is_mouse_over - || self.state.is_scroller_grabbed() - || is_scrollbar_visible - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius, - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); }, ); + }); + + let style = if state.is_scroller_grabbed() { + theme.dragging(style) + } else if is_mouse_over_scrollbar { + theme.hovered(style) } else { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - } + theme.active(style) + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0.0; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if is_scrollbar_visible { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + }, + ); + } else { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); } +} - fn overlay( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option<overlay::Element<'_, Message, Renderer>> { - let Self { content, state, .. } = self; +fn scrollbar( + state: &State, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + bounds: Rectangle, + content_bounds: Rectangle, +) -> Option<Scrollbar> { + let offset = state.offset(bounds, content_bounds); + + if content_bounds.height > bounds.height { + let outer_width = + scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + + let outer_bounds = Rectangle { + x: bounds.x + bounds.width - outer_width as f32, + y: bounds.y, + width: outer_width as f32, + height: bounds.height, + }; - content - .overlay(layout.children().next().unwrap(), renderer) - .map(|overlay| { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = state.offset(bounds, content_bounds); + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scrollbar_width / 2), + y: bounds.y, + width: scrollbar_width as f32, + height: bounds.height, + }; - overlay.translate(Vector::new(0.0, -(offset as f32))) - }) + let ratio = bounds.height / content_bounds.height; + let scroller_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scroller_width / 2), + y: scrollbar_bounds.y + y_offset, + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + outer_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } +} + +fn notify_on_scroll<Message>( + state: &State, + on_scroll: &Option<Box<dyn Fn(f32) -> Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = on_scroll { + shell.publish(on_scroll( + state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); } } @@ -608,6 +827,12 @@ impl Default for State { } } +impl operation::Scrollable for State { + fn snap_to(&mut self, percentage: f32) { + State::snap_to(self, percentage); + } +} + /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { @@ -752,16 +977,3 @@ struct Scroller { /// The bounds of the [`Scroller`]. bounds: Rectangle, } - -impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn from( - scrollable: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(scrollable) - } -} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 289f75f5..585d9c35 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -6,6 +6,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::touch; +use crate::widget::tree::{self, Tree}; use crate::{ Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, Shell, Size, Widget, @@ -13,7 +14,7 @@ use crate::{ use std::ops::RangeInclusive; -pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; +pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; /// An horizontal bar and a handle that selects a single value from a range of /// values. @@ -25,37 +26,44 @@ pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; /// /// # Example /// ``` -/// # use iced_native::widget::slider::{self, Slider}; +/// # use iced_native::widget::slider; +/// # use iced_native::renderer::Null; +/// # +/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; /// # /// #[derive(Clone)] /// pub enum Message { /// SliderChanged(f32), /// } /// -/// let state = &mut slider::State::new(); /// let value = 50.0; /// -/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged); +/// Slider::new(0.0..=100.0, value, Message::SliderChanged); /// ``` /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message> { - state: &'a mut State, +pub struct Slider<'a, T, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ range: RangeInclusive<T>, step: T, value: T, - on_change: Box<dyn Fn(T) -> Message>, + on_change: Box<dyn Fn(T) -> Message + 'a>, on_release: Option<Message>, width: Length, height: u16, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, T, Message> Slider<'a, T, Message> +impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { /// The default height of a [`Slider`]. pub const DEFAULT_HEIGHT: u16 = 22; @@ -63,20 +71,14 @@ where /// Creates a new [`Slider`]. /// /// It expects: - /// * the local [`State`] of the [`Slider`] /// * an inclusive range of possible values /// * the current value of the [`Slider`] /// * a function that will be called when the [`Slider`] is dragged. /// It receives the new value of the [`Slider`] and must produce a /// `Message`. - pub fn new<F>( - state: &'a mut State, - range: RangeInclusive<T>, - value: T, - on_change: F, - ) -> Self + pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where - F: 'static + Fn(T) -> Message, + F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { value @@ -91,7 +93,6 @@ where }; Slider { - state, value, range, step: T::from(1), @@ -99,7 +100,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style_sheet: Default::default(), + style: Default::default(), } } @@ -129,9 +130,9 @@ where /// Sets the style of the [`Slider`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } @@ -142,26 +143,22 @@ where } } -/// The local state of a [`Slider`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_dragging: bool, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} - impl<'a, T, Message, Renderer> Widget<Message, Renderer> - for Slider<'a, T, Message> + for Slider<'a, T, Message, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + fn width(&self) -> Length { self.width } @@ -185,6 +182,7 @@ where fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -192,197 +190,286 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let is_dragging = self.state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *self.range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *self.range.end() - } else { - let step = self.step.into(); - let start = (*self.range.start()).into(); - let end = (*self.range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - if (self.value.into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((self.on_change)(new_value)); - - self.value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = self.on_release.clone() { - shell.publish(on_release); - } - self.state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::<State>(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) } fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_dragging { - self.style_sheet.dragging() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 2.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), f32::from(bounds.height), border_radius), - }; - - let value = self.value.into() as f32; - let (range_start, range_end) = { - let (start, end) = self.range.clone().into_inner(); - - (start.into() as f32, end.into() as f32) - }; - - let handle_offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) - }; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border_radius: handle_border_radius, - border_width: style.handle.border_width, - border_color: style.handle.border_color, - }, - style.handle.color, - ); + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + self.value, + &self.range, + theme, + self.style, + ) } fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if self.state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } + mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + ) } } -impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>> +impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where T: 'a + Copy + Into<f64> + num_traits::FromPrimitive, Message: 'a + Clone, Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, { - fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message, Renderer> { + fn from( + slider: Slider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { Element::new(slider) } } + +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option<Message>, +) -> event::Status +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = || { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + *range.start() + } else if cursor_position.x >= bounds.x + bounds.width { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T, R>( + renderer: &mut R, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet<Style = <R::Theme as StyleSheet>::Style>, + style: <R::Theme as StyleSheet>::Style, +) where + T: Into<f64> + Copy, + R: crate::Renderer, + R::Theme: StyleSheet, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_dragging { + style_sheet.dragging(style) + } else if is_mouse_over { + style_sheet.hovered(style) + } else { + style_sheet.active(style) + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - 1.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 1.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.height, border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let handle_offset = if range_start >= range_end { + 0.0 + } else { + bounds.width * (value - range_start) / (range_end - range_start) + - handle_width / 2.0 + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + +/// The local state of a [`Slider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct State { + is_dragging: bool, +} + +impl State { + /// Creates a new [`State`]. + pub fn new() -> State { + State::default() + } +} diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index 4135d1b8..9f835893 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -1,6 +1,7 @@ //! Distribute content vertically. use crate::layout; use crate::renderer; +use crate::widget::Tree; use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; /// An amount of empty space. @@ -59,7 +60,9 @@ where fn draw( &self, + _state: &Tree, _renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, _layout: Layout<'_>, _cursor_position: Point, diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 008ab356..aa68bfb8 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,13 +1,16 @@ //! Display vector graphics in your application. use crate::layout; use crate::renderer; -use crate::svg::{self, Handle}; +use crate::svg; +use crate::widget::Tree; use crate::{ ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, }; use std::path::PathBuf; +pub use svg::Handle; + /// A vector graphics image. /// /// An [`Svg`] image resizes smoothly without losing any quality. @@ -109,7 +112,9 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + _theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 6f00c9c8..dab6e874 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -3,45 +3,60 @@ use crate::alignment; use crate::layout; use crate::renderer; use crate::text; -use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; +use crate::widget::Tree; +use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget}; + +use std::borrow::Cow; + +pub use iced_style::text::{Appearance, StyleSheet}; /// A paragraph of text. /// /// # Example /// /// ``` -/// # type Text = iced_native::widget::Text<iced_native::renderer::Null>; +/// # use iced_native::Color; +/// # +/// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>; /// # /// Text::new("I <3 iced!") -/// .color([0.0, 0.0, 1.0]) -/// .size(40); +/// .size(40) +/// .style(Color::from([0.0, 0.0, 1.0])); /// ``` /// ///  -#[derive(Debug)] -pub struct Text<Renderer: text::Renderer> { - content: String, +#[allow(missing_debug_implementations)] +pub struct Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: Cow<'a, str>, size: Option<u16>, - color: Option<Color>, - font: Renderer::Font, width: Length, height: Length, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, + font: Renderer::Font, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<Renderer: text::Renderer> Text<Renderer> { +impl<'a, Renderer> Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ /// Create a new fragment of [`Text`] with the given contents. - pub fn new<T: Into<String>>(label: T) -> Self { + pub fn new(content: impl Into<Cow<'a, str>>) -> Self { Text { - content: label.into(), + content: content.into(), size: None, - color: None, font: Default::default(), width: Length::Shrink, height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, + style: Default::default(), } } @@ -51,20 +66,23 @@ impl<Renderer: text::Renderer> Text<Renderer> { self } - /// Sets the [`Color`] of the [`Text`]. - pub fn color<C: Into<Color>>(mut self, color: C) -> Self { - self.color = Some(color.into()); - self - } - /// Sets the [`Font`] of the [`Text`]. /// - /// [`Font`]: Renderer::Font + /// [`Font`]: crate::text::Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = font.into(); self } + /// Sets the [`Color`] of the [`Text`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } + /// Sets the width of the [`Text`] boundaries. pub fn width(mut self, width: Length) -> Self { self.width = width; @@ -77,7 +95,7 @@ impl<Renderer: text::Renderer> Text<Renderer> { self } - /// Sets the [`HorizontalAlignment`] of the [`Text`]. + /// Sets the [`alignment::Horizontal`] of the [`Text`]. pub fn horizontal_alignment( mut self, alignment: alignment::Horizontal, @@ -86,7 +104,7 @@ impl<Renderer: text::Renderer> Text<Renderer> { self } - /// Sets the [`VerticalAlignment`] of the [`Text`]. + /// Sets the [`alignment::Vertical`] of the [`Text`]. pub fn vertical_alignment( mut self, alignment: alignment::Vertical, @@ -96,9 +114,10 @@ impl<Renderer: text::Renderer> Text<Renderer> { } } -impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer> +impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer> where Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { fn width(&self) -> Length { self.width @@ -115,7 +134,7 @@ where ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = self.size.unwrap_or(renderer.default_size()); + let size = self.size.unwrap_or_else(|| renderer.default_size()); let bounds = limits.max(); @@ -129,7 +148,9 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -140,9 +161,9 @@ where style, layout, &self.content, - self.font.clone(), self.size, - self.color, + self.font.clone(), + theme.appearance(self.style), self.horizontal_alignment, self.vertical_alignment, ); @@ -164,9 +185,9 @@ pub fn draw<Renderer>( style: &renderer::Style, layout: Layout<'_>, content: &str, - font: Renderer::Font, size: Option<u16>, - color: Option<Color>, + font: Renderer::Font, + appearance: Appearance, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, ) where @@ -188,36 +209,51 @@ pub fn draw<Renderer>( renderer.fill_text(crate::text::Text { content, - size: f32::from(size.unwrap_or(renderer.default_size())), + size: f32::from(size.unwrap_or_else(|| renderer.default_size())), bounds: Rectangle { x, y, ..bounds }, - color: color.unwrap_or(style.text_color), + color: appearance.color.unwrap_or(style.text_color), font, horizontal_alignment, vertical_alignment, }); } -impl<'a, Message, Renderer> From<Text<Renderer>> +impl<'a, Message, Renderer> From<Text<'a, Renderer>> for Element<'a, Message, Renderer> where Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, { - fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> { + fn from(text: Text<'a, Renderer>) -> Element<'a, Message, Renderer> { Element::new(text) } } -impl<Renderer: text::Renderer> Clone for Text<Renderer> { +impl<'a, Renderer> Clone for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ fn clone(&self) -> Self { Self { content: self.content.clone(), size: self.size, - color: self.color, - font: self.font.clone(), width: self.width, height: self.height, horizontal_alignment: self.horizontal_alignment, vertical_alignment: self.vertical_alignment, + font: self.font.clone(), + style: self.style, } } } + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(contents: &'a str) -> Self { + Text::new(contents).into() + } +} diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e30e2343..c2d25520 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -19,33 +19,29 @@ use crate::mouse::{self, click}; use crate::renderer; use crate::text::{self, Text}; use crate::touch; +use crate::widget; +use crate::widget::operation::{self, Operation}; +use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, Color, Command, Element, Layout, Length, Padding, Point, + Rectangle, Shell, Size, Vector, Widget, }; -use std::u32; - -pub use iced_style::text_input::{Style, StyleSheet}; +pub use iced_style::text_input::{Appearance, StyleSheet}; /// A field that can be filled with text. /// /// # Example /// ``` -/// # use iced_native::renderer::Null; -/// # use iced_native::widget::text_input; -/// # -/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, Null>; +/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, iced_native::renderer::Null>; /// #[derive(Debug, Clone)] /// enum Message { /// TextInputChanged(String), /// } /// -/// let mut state = text_input::State::new(); /// let value = "Some text"; /// /// let input = TextInput::new( -/// &mut state, /// "This is the placeholder...", /// value, /// Message::TextInputChanged, @@ -54,68 +50,82 @@ pub use iced_style::text_input::{Style, StyleSheet}; /// ``` ///  #[allow(missing_debug_implementations)] -pub struct TextInput<'a, Message, Renderer: text::Renderer> { - state: &'a mut State, +pub struct TextInput<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + id: Option<Id>, placeholder: String, value: Value, is_secure: bool, font: Renderer::Font, width: Length, - max_width: u32, padding: Padding, size: Option<u16>, - on_change: Box<dyn Fn(String) -> Message>, + on_change: Box<dyn Fn(String) -> Message + 'a>, + on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> where Message: Clone, Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { /// Creates a new [`TextInput`]. /// /// It expects: - /// - some [`State`] - /// - a placeholder - /// - the current value - /// - a function that produces a message when the [`TextInput`] changes - pub fn new<F>( - state: &'a mut State, - placeholder: &str, - value: &str, - on_change: F, - ) -> Self + /// - a placeholder, + /// - the current value, and + /// - a function that produces a message when the [`TextInput`] changes. + pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self where - F: 'static + Fn(String) -> Message, + F: 'a + Fn(String) -> Message, { TextInput { - state, + id: None, placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, font: Default::default(), width: Length::Fill, - max_width: u32::MAX, padding: Padding::ZERO, size: None, on_change: Box::new(on_change), + on_paste: None, on_submit: None, - style_sheet: Default::default(), + style: Default::default(), } } + /// Sets the [`Id`] of the [`TextInput`]. + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + /// Converts the [`TextInput`] into a secure password input. pub fn password(mut self) -> Self { self.is_secure = true; self } - /// Sets the [`Font`] of the [`Text`]. + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`]. + pub fn on_paste( + mut self, + on_paste: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_paste = Some(Box::new(on_paste)); + self + } + + /// Sets the [`Font`] of the [`TextInput`]. /// - /// [`Font`]: crate::widget::text::Renderer::Font - /// [`Text`]: crate::widget::Text + /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self @@ -126,12 +136,6 @@ where self } - /// Sets the maximum width of the [`TextInput`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - /// Sets the [`Padding`] of the [`TextInput`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -154,190 +158,38 @@ where /// Sets the style of the [`TextInput`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } - /// Returns the current [`State`] of the [`TextInput`]. - pub fn state(&self) -> &State { - self.state - } -} - -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> -where - Renderer: text::Renderer, -{ /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. + /// [`text_input::Value`] if provided. + /// + /// [`Renderer`]: text::Renderer pub fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, layout: Layout<'_>, cursor_position: Point, value: Option<&Value>, ) { - let value = value.unwrap_or(&self.value); - let secure_value = self.is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(&value); - - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_focused() { - self.style_sheet.focused() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background, - ); - - let text = value.to_string(); - let size = self.size.unwrap_or(renderer.default_size()); - - let (cursor, offset) = if self.state.is_focused() { - match self.state.cursor.state(&value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - position, - self.font.clone(), - ); - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.value_color(), - )), - offset, - ) - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - let (left_position, left_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - left, - self.font.clone(), - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - right, - self.font.clone(), - ); - - let width = right_position - left_position; - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.selection_color(), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let text_width = renderer.measure_width( - if text.is_empty() { - &self.placeholder - } else { - &text - }, - size, - self.font.clone(), - ); - - let render = |renderer: &mut Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); - } - - renderer.fill_text(Text { - content: if text.is_empty() { - &self.placeholder - } else { - &text - }, - color: if text.is_empty() { - self.style_sheet.placeholder_color() - } else { - self.style_sheet.value_color() - }, - font: self.font.clone(), - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size: f32::from(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - }; - - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render) - }); - } else { - render(renderer); - } + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + value.unwrap_or(&self.value), + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style, + ) } } @@ -346,7 +198,16 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> where Message: Clone, Renderer: text::Renderer, + Renderer::Theme: StyleSheet, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + fn width(&self) -> Length { self.width } @@ -360,25 +221,23 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let text_size = self.size.unwrap_or(renderer.default_size()); - - let limits = limits - .pad(self.padding) - .width(self.width) - .max_width(self.max_width) - .height(Length::Units(text_size)); + layout(renderer, limits, self.width, self.padding, self.size) + } - let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + operation: &mut dyn Operation<Message>, + ) { + let state = tree.state.downcast_mut::<State>(); - layout::Node::with_children(text.size().pad(self.padding), vec![text]) + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -386,305 +245,407 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let is_clicked = layout.bounds().contains(cursor_position); + update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + &self.font, + self.is_secure, + self.on_change.as_ref(), + self.on_paste.as_deref(), + &self.on_submit, + || tree.state.downcast_mut::<State>(), + ) + } - self.state.is_focused = is_clicked; + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + theme, + layout, + cursor_position, + tree.state.downcast_ref::<State>(), + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style, + ) + } - if is_clicked { - let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction(layout, cursor_position) + } +} - let click = mouse::Click::new( - cursor_position, - self.state.last_click, - ); +impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + text_input: TextInput<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(text_input) + } +} + +/// The identifier of a [`TextInput`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus<Message: 'static>(id: Id) -> Command<Message> { + Command::widget(operation::focusable::focus(id.0)) +} + +/// Computes the layout of a [`TextInput`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option<u16>, +) -> layout::Node +where + Renderer: text::Renderer, +{ + let text_size = size.unwrap_or_else(|| renderer.default_size()); + + let limits = limits + .pad(padding) + .width(width) + .height(Length::Units(text_size)); + + let mut text = layout::Node::new(limits.resolve(Size::ZERO)); + text.move_to(Point::new(padding.left.into(), padding.top.into())); + + layout::Node::with_children(text.size().pad(padding), vec![text]) +} - match click.kind() { - click::Kind::Single => { - let position = if target > 0.0 { - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - - find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +pub fn update<'a, Message, Renderer>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + on_change: &dyn Fn(String) -> Message, + on_paste: Option<&dyn Fn(String) -> Message>, + on_submit: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status +where + Message: Clone, + Renderer: text::Renderer, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + let is_clicked = layout.bounds().contains(cursor_position); + + state.is_focused = is_clicked; + + if is_clicked { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + let click = + mouse::Click::new(cursor_position, state.last_click); + + match click.kind() { + click::Kind::Single => { + let position = if target > 0.0 { + let value = if is_secure { + value.secure() } else { - None + value.clone() }; - self.state.cursor.move_to(position.unwrap_or(0)); - self.state.is_dragging = true; - } - click::Kind::Double => { - if self.is_secure { - self.state.cursor.select_all(&self.value); - } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &self.value, - &self.state, - target, - ) - .unwrap_or(0); - - self.state.cursor.select_range( - self.value.previous_start_of_word(position), - self.value.next_end_of_word(position), - ); - } + find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + } else { + None + }; - self.state.is_dragging = false; - } - click::Kind::Triple => { - self.state.cursor.select_all(&self.value); - self.state.is_dragging = false; + state.cursor.move_to(position.unwrap_or(0)); + state.is_dragging = true; + } + click::Kind::Double => { + if is_secure { + state.cursor.select_all(value); + } else { + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(value); + state.is_dragging = false; } + } - self.state.last_click = Some(click); + state.last_click = Some(click); - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_dragging = false; + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - if self.state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state().is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state(); - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) - .unwrap_or(0); - - self.state.cursor.select_range( - self.state.cursor.start(&value), - position, - ); + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; - return event::Status::Captured; - } + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) - if self.state.is_focused - && self.state.is_pasting.is_none() - && !self.state.keyboard_modifiers.command() - && !c.is_control() => + } + Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { + let state = state(); + + if state.is_focused + && state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() { - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new(value, &mut state.cursor); editor.insert(c); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyPressed { - key_code, .. - }) if self.state.is_focused => { - let modifiers = self.state.keyboard_modifiers; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + let state = state(); + + if state.is_focused { + let modifiers = state.keyboard_modifiers; match key_code { keyboard::KeyCode::Enter | keyboard::KeyCode::NumpadEnter => { - if let Some(on_submit) = self.on_submit.clone() { + if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } keyboard::KeyCode::Backspace => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(0, cursor_pos); } else { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.backspace(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state + if is_secure { + let cursor_pos = state.cursor.end(value); + state .cursor - .select_range(cursor_pos, self.value.len()); + .select_range(cursor_pos, value.len()); } else { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } else { - self.state - .cursor - .move_left_by_words(&self.value); + state.cursor.move_left_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_left(&self.value) + state.cursor.select_left(value) } else { - self.state.cursor.move_left(&self.value); + state.cursor.move_left(value); } } keyboard::KeyCode::Right => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } else { - self.state - .cursor - .move_right_by_words(&self.value); + state.cursor.move_right_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_right(&self.value) + state.cursor.select_right(value) } else { - self.state.cursor.move_right(&self.value); + state.cursor.move_right(value); } } keyboard::KeyCode::Home => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); + state + .cursor + .select_range(state.cursor.start(value), 0); } else { - self.state.cursor.move_to(0); + state.cursor.move_to(0); } } keyboard::KeyCode::End => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), + state.cursor.select_range( + state.cursor.start(value), + value.len(), ); } else { - self.state.cursor.move_to(self.value.len()); + state.cursor.move_to(value.len()); } } keyboard::KeyCode::C - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { - Some((start, end)) => { - clipboard.write( - self.value.select(start, end).to_string(), - ); - } - None => {} + if let Some((start, end)) = + state.cursor.selection(value) + { + clipboard + .write(value.select(start, end).to_string()); } } keyboard::KeyCode::X - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { - Some((start, end)) => { - clipboard.write( - self.value.select(start, end).to_string(), - ); - } - None => {} + if let Some((start, end)) = + state.cursor.selection(value) + { + clipboard + .write(value.select(start, end).to_string()); } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::V => { - if self.state.keyboard_modifiers.command() { - let content = match self.state.is_pasting.take() { + if state.keyboard_modifiers.command() { + let content = match state.is_pasting.take() { Some(content) => content, None => { let content: String = clipboard .read() - .unwrap_or(String::new()) + .unwrap_or_default() .chars() .filter(|c| !c.is_control()) .collect(); @@ -693,32 +654,34 @@ where } }; - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); + let mut editor = + Editor::new(value, &mut state.cursor); editor.paste(content.clone()); - let message = (self.on_change)(editor.contents()); + let message = if let Some(paste) = &on_paste { + (paste)(editor.contents()) + } else { + (on_change)(editor.contents()) + }; shell.publish(message); - self.state.is_pasting = Some(content); + state.is_pasting = Some(content); } else { - self.state.is_pasting = None; + state.is_pasting = None; } } keyboard::KeyCode::A - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - self.state.cursor.select_all(&self.value); + state.cursor.select_all(value); } keyboard::KeyCode::Escape => { - self.state.is_focused = false; - self.state.is_dragging = false; - self.state.is_pasting = None; + state.is_focused = false; + state.is_dragging = false; + state.is_pasting = None; - self.state.keyboard_modifiers = + state.keyboard_modifiers = keyboard::Modifiers::default(); } keyboard::KeyCode::Tab @@ -731,12 +694,14 @@ where return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyReleased { - key_code, .. - }) if self.state.is_focused => { + } + Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + let state = state(); + + if state.is_focused { match key_code { keyboard::KeyCode::V => { - self.state.is_pasting = None; + state.is_pasting = None; } keyboard::KeyCode::Tab | keyboard::KeyCode::Up @@ -748,53 +713,201 @@ where return event::Status::Captured; } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) - if self.state.is_focused => - { - self.state.keyboard_modifiers = modifiers; - } - _ => {} } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); - event::Status::Ignored + if state.is_focused { + state.keyboard_modifiers = modifiers; + } + } + _ => {} } - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Text - } else { - mouse::Interaction::default() + event::Status::Ignored +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +/// +/// [`Renderer`]: text::Renderer +pub fn draw<Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: &Value, + placeholder: &str, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + style: <Renderer::Theme as StyleSheet>::Style, +) where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + let appearance = if state.is_focused() { + theme.focused(style) + } else if is_mouse_over { + theme.hovered(style) + } else { + theme.active(style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + let text = value.to_string(); + let size = size.unwrap_or_else(|| renderer.default_size()); + + let (cursor, offset) = if state.is_focused() { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + position, + font.clone(), + ); + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(style), + )), + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + left, + font.clone(), + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + value, + size, + right, + font.clone(), + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.selection_color(style), + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { placeholder } else { &text }, + size, + font.clone(), + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); } - } - fn draw( - &self, - renderer: &mut Renderer, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - self.draw(renderer, layout, cursor_position, None) + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color: if text.is_empty() { + theme.placeholder_color(style) + } else { + theme.value_color(style) + }, + font: font.clone(), + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render) + }); + } else { + render(renderer); } } -impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, -{ - fn from( - text_input: TextInput<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Text + } else { + mouse::Interaction::default() } } @@ -841,6 +954,7 @@ impl State { /// Focuses the [`TextInput`]. pub fn focus(&mut self) { self.is_focused = true; + self.move_cursor_to_end(); } /// Unfocuses the [`TextInput`]. @@ -869,6 +983,20 @@ impl State { } } +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + State::is_focused(self) + } + + fn focus(&mut self) { + State::focus(self) + } + + fn unfocus(&mut self) { + State::unfocus(self) + } +} + mod platform { use crate::keyboard; @@ -950,16 +1078,16 @@ fn find_cursor_position<Renderer>( where Renderer: text::Renderer, { - let size = size.unwrap_or(renderer.default_size()); + let size = size.unwrap_or_else(|| renderer.default_size()); let offset = - offset(renderer, text_bounds, font.clone(), size, &value, &state); + offset(renderer, text_bounds, font.clone(), size, value, state); renderer .hit_test( &value.to_string(), size.into(), - font.clone(), + font, Size::INFINITY, Point::new(x + offset, text_bounds.height / 2.0), true, diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs index bac530e1..d53fa8d9 100644 --- a/native/src/widget/text_input/editor.rs +++ b/native/src/widget/text_input/editor.rs @@ -15,12 +15,9 @@ impl<'a> Editor<'a> { } pub fn insert(&mut self, character: char) { - match self.cursor.selection(self.value) { - Some((left, right)) => { - self.cursor.move_left(self.value); - self.value.remove_many(left, right); - } - _ => {} + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); } self.value.insert(self.cursor.end(self.value), character); @@ -29,13 +26,9 @@ impl<'a> Editor<'a> { pub fn paste(&mut self, content: Value) { let length = content.len(); - - match self.cursor.selection(self.value) { - Some((left, right)) => { - self.cursor.move_left(self.value); - self.value.remove_many(left, right); - } - _ => {} + if let Some((left, right)) = self.cursor.selection(self.value) { + self.cursor.move_left(self.value); + self.value.remove_many(left, right); } self.value.insert_many(self.cursor.end(self.value), content); diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs index 2034cca4..cf4da562 100644 --- a/native/src/widget/text_input/value.rs +++ b/native/src/widget/text_input/value.rs @@ -37,7 +37,7 @@ impl Value { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); - UnicodeSegmentation::split_word_bound_indices(&previous_string as &str) + UnicodeSegmentation::split_word_bound_indices(previous_string as &str) .filter(|(_, word)| !word.trim_start().is_empty()) .next_back() .map(|(i, previous_word)| { @@ -58,9 +58,8 @@ impl Value { pub fn next_end_of_word(&self, index: usize) -> usize { let next_string = &self.graphemes[index..].concat(); - UnicodeSegmentation::split_word_bound_indices(&next_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next() + UnicodeSegmentation::split_word_bound_indices(next_string as &str) + .find(|(_, word)| !word.trim_start().is_empty()) .map(|(i, next_word)| { index + UnicodeSegmentation::graphemes(next_word, true).count() diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 48237edb..7893f78c 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -1,20 +1,19 @@ //! Show toggle controls using togglers. - use crate::alignment; use crate::event; use crate::layout; use crate::mouse; use crate::renderer; use crate::text; -use crate::widget::{Row, Text}; +use crate::widget::{self, Row, Text, Tree}; use crate::{ Alignment, Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Widget, }; -pub use iced_style::toggler::{Style, StyleSheet}; +pub use iced_style::toggler::{Appearance, StyleSheet}; -/// A toggler widget +/// A toggler widget. /// /// # Example /// @@ -30,9 +29,13 @@ pub use iced_style::toggler::{Style, StyleSheet}; /// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); /// ``` #[allow(missing_debug_implementations)] -pub struct Toggler<'a, Message, Renderer: text::Renderer> { +pub struct Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ is_active: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: Option<String>, width: Length, size: u16, @@ -40,10 +43,14 @@ pub struct Toggler<'a, Message, Renderer: text::Renderer> { text_alignment: alignment::Horizontal, spacing: u16, font: Renderer::Font, - style_sheet: Box<dyn StyleSheet + 'a>, + style: <Renderer::Theme as StyleSheet>::Style, } -impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { +impl<'a, Message, Renderer> Toggler<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ /// The default size of a [`Toggler`]. pub const DEFAULT_SIZE: u16 = 20; @@ -61,7 +68,7 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { f: F, ) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Toggler { is_active, @@ -73,7 +80,7 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { text_alignment: alignment::Horizontal::Left, spacing: 0, font: Renderer::Font::default(), - style_sheet: Default::default(), + style: Default::default(), } } @@ -108,6 +115,8 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { } /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Font`]: crate::text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self @@ -116,9 +125,9 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { /// Sets the style of the [`Toggler`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } @@ -127,6 +136,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> for Toggler<'a, Message, Renderer> where Renderer: text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { fn width(&self) -> Length { self.width @@ -152,7 +162,10 @@ where .horizontal_alignment(self.text_alignment) .font(self.font.clone()) .width(self.width) - .size(self.text_size.unwrap_or(renderer.default_size())), + .size( + self.text_size + .unwrap_or_else(|| renderer.default_size()), + ), ); } @@ -167,6 +180,7 @@ where fn on_event( &mut self, + _state: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -192,6 +206,7 @@ where fn mouse_interaction( &self, + _state: &Tree, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, @@ -206,7 +221,9 @@ where fn draw( &self, + _state: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -228,10 +245,10 @@ where renderer, style, label_layout, - &label, - self.font.clone(), + label, self.text_size, - None, + self.font.clone(), + Default::default(), self.text_alignment, alignment::Vertical::Center, ); @@ -243,9 +260,9 @@ where let is_mouse_over = bounds.contains(cursor_position); let style = if is_mouse_over { - self.style_sheet.hovered(self.is_active) + theme.hovered(self.style, self.is_active) } else { - self.style_sheet.active(self.is_active) + theme.active(self.style, self.is_active) }; let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO; @@ -299,8 +316,9 @@ where impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>> for Element<'a, Message, Renderer> where - Renderer: 'a + text::Renderer, Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: StyleSheet + widget::text::StyleSheet, { fn from( toggler: Toggler<'a, Message, Renderer>, diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs index 7989c768..674f2ba6 100644 --- a/native/src/widget/tooltip.rs +++ b/native/src/widget/tooltip.rs @@ -4,46 +4,57 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::text; +use crate::widget; use crate::widget::container; -use crate::widget::text::Text; +use crate::widget::overlay; +use crate::widget::{Text, Tree}; use crate::{ Clipboard, Element, Event, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, }; +use std::borrow::Cow; + /// An element to display a widget over another. #[allow(missing_debug_implementations)] -pub struct Tooltip<'a, Message, Renderer: text::Renderer> { +pub struct Tooltip<'a, Message, Renderer: text::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ content: Element<'a, Message, Renderer>, - tooltip: Text<Renderer>, + tooltip: Text<'a, Renderer>, position: Position, - style_sheet: Box<dyn container::StyleSheet + 'a>, gap: u16, padding: u16, + snap_within_viewport: bool, + style: <Renderer::Theme as container::StyleSheet>::Style, } impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer> where Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, { /// The default padding of a [`Tooltip`] drawn by this renderer. const DEFAULT_PADDING: u16 = 5; - /// Creates an empty [`Tooltip`]. + /// Creates a new [`Tooltip`]. /// /// [`Tooltip`]: struct.Tooltip.html pub fn new( content: impl Into<Element<'a, Message, Renderer>>, - tooltip: impl ToString, + tooltip: impl Into<Cow<'a, str>>, position: Position, ) -> Self { Tooltip { content: content.into(), - tooltip: Text::new(tooltip.to_string()), + tooltip: Text::new(tooltip), position, - style_sheet: Default::default(), gap: 0, padding: Self::DEFAULT_PADDING, + snap_within_viewport: true, + style: Default::default(), } } @@ -73,42 +84,42 @@ where self } + /// Sets whether the [`Tooltip`] is snapped within the viewport. + pub fn snap_within_viewport(mut self, snap: bool) -> Self { + self.snap_within_viewport = snap; + self + } + /// Sets the style of the [`Tooltip`]. pub fn style( mut self, - style_sheet: impl Into<Box<dyn container::StyleSheet + 'a>>, + style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>, ) -> Self { - self.style_sheet = style_sheet.into(); + self.style = style.into(); self } } -/// The position of the tooltip. Defaults to following the cursor. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Position { - /// The tooltip will follow the cursor. - FollowCursor, - /// The tooltip will appear on the top of the widget. - Top, - /// The tooltip will appear on the bottom of the widget. - Bottom, - /// The tooltip will appear on the left of the widget. - Left, - /// The tooltip will appear on the right of the widget. - Right, -} - impl<'a, Message, Renderer> Widget<Message, Renderer> for Tooltip<'a, Message, Renderer> where Renderer: text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, { + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + fn width(&self) -> Length { - self.content.width() + self.content.as_widget().width() } fn height(&self) -> Length { - self.content.height() + self.content.as_widget().height() } fn layout( @@ -116,11 +127,12 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.layout(renderer, limits) + self.content.as_widget().layout(renderer, limits) } fn on_event( &mut self, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -128,7 +140,8 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.content.widget.on_event( + self.content.as_widget_mut().on_event( + &mut tree.children[0], event, layout, cursor_position, @@ -140,12 +153,14 @@ where fn mouse_interaction( &self, + tree: &Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content.mouse_interaction( + self.content.as_widget().mouse_interaction( + &tree.children[0], layout, cursor_position, viewport, @@ -155,77 +170,185 @@ where fn draw( &self, + tree: &Tree, renderer: &mut Renderer, + theme: &Renderer::Theme, inherited_style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, ) { - self.content.draw( + self.content.as_widget().draw( + &tree.children[0], renderer, + theme, inherited_style, layout, cursor_position, viewport, ); - let bounds = layout.bounds(); + let tooltip = &self.tooltip; - if bounds.contains(cursor_position) { - let gap = f32::from(self.gap); - let style = self.style_sheet.style(); + draw( + renderer, + theme, + inherited_style, + layout, + cursor_position, + viewport, + self.position, + self.gap, + self.padding, + self.snap_within_viewport, + self.style, + |renderer, limits| { + Widget::<(), Renderer>::layout(tooltip, renderer, limits) + }, + |renderer, defaults, layout, cursor_position, viewport| { + Widget::<(), Renderer>::draw( + tooltip, + &Tree::empty(), + renderer, + theme, + defaults, + layout, + cursor_position, + viewport, + ); + }, + ); + } - let defaults = renderer::Style { - text_color: style - .text_color - .unwrap_or(inherited_style.text_color), - }; + fn overlay<'b>( + &'b self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } +} - let text_layout = Widget::<(), Renderer>::layout( - &self.tooltip, - renderer, - &layout::Limits::new(Size::ZERO, viewport.size()) - .pad(Padding::new(self.padding)), - ); - - let padding = f32::from(self.padding); - let text_bounds = text_layout.bounds(); - let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; - let y_center = - bounds.y + (bounds.height - text_bounds.height) / 2.0; - - let mut tooltip_bounds = { - let offset = match self.position { - Position::Top => Vector::new( - x_center, - bounds.y - text_bounds.height - gap - padding, - ), - Position::Bottom => Vector::new( - x_center, - bounds.y + bounds.height + gap + padding, - ), - Position::Left => Vector::new( - bounds.x - text_bounds.width - gap - padding, - y_center, - ), - Position::Right => Vector::new( - bounds.x + bounds.width + gap + padding, - y_center, - ), - Position::FollowCursor => Vector::new( - cursor_position.x, - cursor_position.y - text_bounds.height, - ), - }; - - Rectangle { - x: offset.x - padding, - y: offset.y - padding, - width: text_bounds.width + padding * 2.0, - height: text_bounds.height + padding * 2.0, - } +impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + text::Renderer, + Renderer::Theme: container::StyleSheet + widget::text::StyleSheet, +{ + fn from( + tooltip: Tooltip<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(tooltip) + } +} + +/// The position of the tooltip. Defaults to following the cursor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Position { + /// The tooltip will follow the cursor. + FollowCursor, + /// The tooltip will appear on the top of the widget. + Top, + /// The tooltip will appear on the bottom of the widget. + Bottom, + /// The tooltip will appear on the left of the widget. + Left, + /// The tooltip will appear on the right of the widget. + Right, +} + +/// Draws a [`Tooltip`]. +pub fn draw<Renderer>( + renderer: &mut Renderer, + theme: &Renderer::Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + position: Position, + gap: u16, + padding: u16, + snap_within_viewport: bool, + style: <Renderer::Theme as container::StyleSheet>::Style, + layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, + draw_text: impl FnOnce( + &mut Renderer, + &renderer::Style, + Layout<'_>, + Point, + &Rectangle, + ), +) where + Renderer: crate::Renderer, + Renderer::Theme: container::StyleSheet, +{ + use container::StyleSheet; + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let gap = f32::from(gap); + let style = theme.appearance(style); + + let defaults = renderer::Style { + text_color: style.text_color.unwrap_or(inherited_style.text_color), + }; + + let text_layout = layout_text( + renderer, + &layout::Limits::new( + Size::ZERO, + snap_within_viewport + .then(|| viewport.size()) + .unwrap_or(Size::INFINITY), + ) + .pad(Padding::new(padding)), + ); + + let padding = f32::from(padding); + let text_bounds = text_layout.bounds(); + let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0; + let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0; + + let mut tooltip_bounds = { + let offset = match position { + Position::Top => Vector::new( + x_center, + bounds.y - text_bounds.height - gap - padding, + ), + Position::Bottom => Vector::new( + x_center, + bounds.y + bounds.height + gap + padding, + ), + Position::Left => Vector::new( + bounds.x - text_bounds.width - gap - padding, + y_center, + ), + Position::Right => Vector::new( + bounds.x + bounds.width + gap + padding, + y_center, + ), + Position::FollowCursor => Vector::new( + cursor_position.x, + cursor_position.y - text_bounds.height, + ), }; + Rectangle { + x: offset.x - padding, + y: offset.y - padding, + width: text_bounds.width + padding * 2.0, + height: text_bounds.height + padding * 2.0, + } + }; + + if snap_within_viewport { if tooltip_bounds.x < viewport.x { tooltip_bounds.x = viewport.x; } else if viewport.x + viewport.width @@ -243,38 +366,24 @@ where tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height; } + } - renderer.with_layer(*viewport, |renderer| { - container::draw_background(renderer, &style, tooltip_bounds); + renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| { + container::draw_background(renderer, &style, tooltip_bounds); - Widget::<(), Renderer>::draw( - &self.tooltip, - renderer, - &defaults, - Layout::with_offset( - Vector::new( - tooltip_bounds.x + padding, - tooltip_bounds.y + padding, - ), - &text_layout, + draw_text( + renderer, + &defaults, + Layout::with_offset( + Vector::new( + tooltip_bounds.x + padding, + tooltip_bounds.y + padding, ), - cursor_position, - viewport, - ); - }); - } - } -} - -impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Renderer: 'a + text::Renderer, - Message: 'a, -{ - fn from( - column: Tooltip<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) + &text_layout, + ), + cursor_position, + viewport, + ) + }); } } diff --git a/native/src/widget/tree.rs b/native/src/widget/tree.rs new file mode 100644 index 00000000..a8b1a185 --- /dev/null +++ b/native/src/widget/tree.rs @@ -0,0 +1,187 @@ +//! Store internal widget state in a state tree to ensure continuity. +use crate::Widget; + +use std::any::{self, Any}; +use std::borrow::Borrow; +use std::fmt; + +/// A persistent state widget tree. +/// +/// A [`Tree`] is normally associated with a specific widget in the widget tree. +#[derive(Debug)] +pub struct Tree { + /// The tag of the [`Tree`]. + pub tag: Tag, + + /// The [`State`] of the [`Tree`]. + pub state: State, + + /// The children of the root widget of the [`Tree`]. + pub children: Vec<Tree>, +} + +impl Tree { + /// Creates an empty, stateless [`Tree`] with no children. + pub fn empty() -> Self { + Self { + tag: Tag::stateless(), + state: State::None, + children: Vec::new(), + } + } + + /// Creates a new [`Tree`] for the provided [`Element`]. + pub fn new<'a, Message, Renderer>( + widget: impl Borrow<dyn Widget<Message, Renderer> + 'a>, + ) -> Self + where + Renderer: crate::Renderer, + { + let widget = widget.borrow(); + + Self { + tag: widget.tag(), + state: widget.state(), + children: widget.children(), + } + } + + /// Reconciliates the current tree with the provided [`Element`]. + /// + /// If the tag of the [`Element`] matches the tag of the [`Tree`], then the + /// [`Element`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). + /// + /// Otherwise, the whole [`Tree`] is recreated. + /// + /// [`Widget::diff`]: crate::Widget::diff + pub fn diff<'a, Message, Renderer>( + &mut self, + new: impl Borrow<dyn Widget<Message, Renderer> + 'a>, + ) where + Renderer: crate::Renderer, + { + if self.tag == new.borrow().tag() { + new.borrow().diff(self) + } else { + *self = Self::new(new); + } + } + + /// Reconciliates the children of the tree with the provided list of [`Element`]. + pub fn diff_children<'a, Message, Renderer>( + &mut self, + new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>], + ) where + Renderer: crate::Renderer, + { + self.diff_children_custom( + new_children, + |tree, widget| tree.diff(widget.borrow()), + |widget| Self::new(widget.borrow()), + ) + } + + /// Reconciliates the children of the tree with the provided list of [`Element`] using custom + /// logic both for diffing and creating new widget state. + pub fn diff_children_custom<T>( + &mut self, + new_children: &[T], + diff: impl Fn(&mut Tree, &T), + new_state: impl Fn(&T) -> Self, + ) { + if self.children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + diff(child_state, new); + } + + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(new_state), + ); + } + } +} + +/// The identifier of some widget state. +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + /// Creates a [`Tag`] for a state of type `T`. + pub fn of<T>() -> Self + where + T: 'static, + { + Self(any::TypeId::of::<T>()) + } + + /// Creates a [`Tag`] for a stateless widget. + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +/// The internal [`State`] of a widget. +pub enum State { + /// No meaningful internal state. + None, + + /// Some meaningful internal state. + Some(Box<dyn Any>), +} + +impl State { + /// Creates a new [`State`]. + pub fn new<T>(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + + /// Downcasts the [`State`] to `T` and returns a reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_ref<T>(&self) -> &T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } + } + + /// Downcasts the [`State`] to `T` and returns a mutable reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_mut<T>(&mut self) -> &mut T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::Some(_) => write!(f, "State::Some"), + } + } +} diff --git a/native/src/window.rs b/native/src/window.rs index 62487fb9..f910b8f2 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,6 +1,8 @@ //! Build window-based GUI applications. mod action; mod event; +mod mode; pub use action::Action; pub use event::Event; +pub use mode::Mode; diff --git a/native/src/window/action.rs b/native/src/window/action.rs index 01294e83..73338e22 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,6 +1,10 @@ +use crate::window::Mode; + +use iced_futures::MaybeSend; +use std::fmt; + /// An operation to be performed on some window. -#[derive(Debug)] -pub enum Action { +pub enum Action<T> { /// Resize the window. Resize { /// The new logical width of the window @@ -9,10 +13,51 @@ pub enum Action { height: u32, }, /// Move the window. + /// + /// Unsupported on Wayland. Move { /// The new logical x location of the window x: i32, /// The new logical y location of the window y: i32, }, + /// Set the [`Mode`] of the window. + SetMode(Mode), + /// Fetch the current [`Mode`] of the window. + FetchMode(Box<dyn FnOnce(Mode) -> T + 'static>), +} + +impl<T> Action<T> { + /// Maps the output of a window [`Action`] using the provided closure. + pub fn map<A>( + self, + f: impl Fn(T) -> A + 'static + MaybeSend + Sync, + ) -> Action<A> + where + T: 'static, + { + match self { + Self::Resize { width, height } => Action::Resize { width, height }, + Self::Move { x, y } => Action::Move { x, y }, + Self::SetMode(mode) => Action::SetMode(mode), + Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))), + } + } +} + +impl<T> fmt::Debug for Action<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Resize { width, height } => write!( + f, + "Action::Resize {{ widget: {}, height: {} }}", + width, height + ), + Self::Move { x, y } => { + write!(f, "Action::Move {{ x: {}, y: {} }}", x, y) + } + Self::SetMode(mode) => write!(f, "Action::SetMode({:?})", mode), + Self::FetchMode(_) => write!(f, "Action::FetchMode"), + } + } } diff --git a/native/src/window/event.rs b/native/src/window/event.rs index 691af29a..86321ac0 100644 --- a/native/src/window/event.rs +++ b/native/src/window/event.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; /// A window-related event. -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Eq, Clone, Debug)] pub enum Event { /// A window was moved. Moved { diff --git a/src/window/mode.rs b/native/src/window/mode.rs index fdce8e23..fdce8e23 100644 --- a/src/window/mode.rs +++ b/native/src/window/mode.rs diff --git a/src/application.rs b/src/application.rs index 14a16d61..23ce034e 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,5 +1,7 @@ -use crate::window; -use crate::{Color, Command, Element, Executor, Settings, Subscription}; +//! Build interactive cross-platform applications. +use crate::{Command, Element, Executor, Settings, Subscription}; + +pub use iced_native::application::{Appearance, StyleSheet}; /// An interactive cross-platform application. /// @@ -37,15 +39,15 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription}; /// to listen to time. /// - [`todos`], a todos tracker inspired by [TodoMVC]. /// -/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/0.3/examples/clock -/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.3/examples/download_progress -/// [`events`]: https://github.com/hecrj/iced/tree/0.3/examples/events -/// [`game_of_life`]: https://github.com/hecrj/iced/tree/0.3/examples/game_of_life -/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.3/examples/pokedex -/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.3/examples/solar_system -/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.3/examples/stopwatch -/// [`todos`]: https://github.com/hecrj/iced/tree/0.3/examples/todos +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`clock`]: https://github.com/iced-rs/iced/tree/0.4/examples/clock +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.4/examples/download_progress +/// [`events`]: https://github.com/iced-rs/iced/tree/0.4/examples/events +/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.4/examples/game_of_life +/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.4/examples/pokedex +/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.4/examples/solar_system +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.4/examples/stopwatch +/// [`todos`]: https://github.com/iced-rs/iced/tree/0.4/examples/todos /// [`Sandbox`]: crate::Sandbox /// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ @@ -57,7 +59,8 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription}; /// says "Hello, world!": /// /// ```no_run -/// use iced::{executor, Application, Command, Element, Settings, Text}; +/// use iced::executor; +/// use iced::{Application, Command, Element, Settings, Theme}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -67,8 +70,9 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription}; /// /// impl Application for Hello { /// type Executor = executor::Default; -/// type Message = (); /// type Flags = (); +/// type Message = (); +/// type Theme = Theme; /// /// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { /// (Hello, Command::none()) @@ -82,8 +86,8 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription}; /// Command::none() /// } /// -/// fn view(&mut self) -> Element<Self::Message> { -/// Text::new("Hello, world!").into() +/// fn view(&self) -> Element<Self::Message> { +/// "Hello, world!".into() /// } /// } /// ``` @@ -99,6 +103,9 @@ pub trait Application: Sized { /// 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; @@ -129,6 +136,26 @@ pub trait Application: Sized { /// 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 [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> Element<'_, Self::Message, crate::Renderer<Self::Theme>>; + + /// Returns the current [`Theme`] of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + fn theme(&self) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current [`Style`] of the [`Theme`]. + /// + /// [`Style`]: <Self::Theme as StyleSheet>::Style + /// [`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. /// @@ -141,30 +168,6 @@ pub trait Application: Sized { Subscription::none() } - /// Returns the widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message>; - - /// Returns the current [`Application`] mode. - /// - /// The runtime will automatically transition your application if a new mode - /// is returned. - /// - /// Currently, the mode only has an effect in native platforms. - /// - /// By default, an application will run in windowed mode. - fn mode(&self) -> window::Mode { - window::Mode::Windowed - } - - /// Returns the background color of the [`Application`]. - /// - /// By default, it returns [`Color::WHITE`]. - fn background_color(&self) -> Color { - Color::WHITE - } - /// Returns the scale factor of the [`Application`]. /// /// It can be used to dynamically control the size of the UI at runtime @@ -198,6 +201,7 @@ pub trait Application: Sized { 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, @@ -213,7 +217,7 @@ pub trait Application: Sized { Ok(crate::runtime::application::run::< Instance<Self>, Self::Executor, - crate::renderer::window::Compositor, + crate::renderer::window::Compositor<Self::Theme>, >(settings.into(), renderer_settings)?) } } @@ -224,14 +228,14 @@ impl<A> iced_winit::Program for Instance<A> where A: Application, { - type Renderer = crate::renderer::Renderer; + 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(&mut self) -> Element<'_, Self::Message> { + fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { self.0.view() } } @@ -252,20 +256,16 @@ where self.0.title() } - fn mode(&self) -> iced_winit::Mode { - match self.0.mode() { - window::Mode::Windowed => iced_winit::Mode::Windowed, - window::Mode::Fullscreen => iced_winit::Mode::Fullscreen, - window::Mode::Hidden => iced_winit::Mode::Hidden, - } + fn theme(&self) -> A::Theme { + self.0.theme() } - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() + fn style(&self) -> <A::Theme as StyleSheet>::Style { + self.0.style() } - fn background_color(&self) -> Color { - self.0.background_color() + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() } fn scale_factor(&self) -> f64 { diff --git a/src/element.rs b/src/element.rs index 8bad18c1..2eb1bb4d 100644 --- a/src/element.rs +++ b/src/element.rs @@ -1,5 +1,5 @@ /// A generic widget. /// /// This is an alias of an `iced_native` element with a default `Renderer`. -pub type Element<'a, Message> = - crate::runtime::Element<'a, Message, crate::renderer::Renderer>; +pub type Element<'a, Message, Renderer = crate::Renderer> = + crate::runtime::Element<'a, Message, Renderer>; diff --git a/src/error.rs b/src/error.rs index 17479c60..0bfa3ff1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,9 +11,9 @@ pub enum Error { #[error("the application window could not be created")] WindowCreationFailed(Box<dyn std::error::Error + Send + Sync>), - /// A suitable graphics adapter or device could not be found. - #[error("a suitable graphics adapter or device could not be found")] - GraphicsAdapterNotFound, + /// The application graphics context could not be created. + #[error("the application graphics context could not be created")] + GraphicsCreationFailed(iced_graphics::Error), } impl From<iced_winit::Error> for Error { @@ -25,8 +25,8 @@ impl From<iced_winit::Error> for Error { iced_winit::Error::WindowCreationFailed(error) => { Error::WindowCreationFailed(Box::new(error)) } - iced_winit::Error::GraphicsAdapterNotFound => { - Error::GraphicsAdapterNotFound + iced_winit::Error::GraphicsCreationFailed(error) => { + Error::GraphicsCreationFailed(error) } } } @@ -19,19 +19,19 @@ //! //! Check out the [repository] and the [examples] for more details! //! -//! [Cross-platform support]: https://github.com/hecrj/iced/blob/master/docs/images/todos_desktop.jpg?raw=true +//! [Cross-platform support]: https://github.com/iced-rs/iced/blob/master/docs/images/todos_desktop.jpg?raw=true //! [text inputs]: https://gfycat.com/alertcalmcrow-rust-gui //! [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui //! [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee -//! [Modular ecosystem]: https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/hecrj/iced/tree/master/native +//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md +//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/0.4/master/native //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/hecrj/iced/tree/master/wgpu -//! [windowing shell]: https://github.com/hecrj/iced/tree/master/winit +//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.4/wgpu +//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.4/winit //! [`dodrio`]: https://github.com/fitzgen/dodrio -//! [web runtime]: https://github.com/hecrj/iced/tree/master/web -//! [examples]: https://github.com/hecrj/iced/tree/0.3/examples -//! [repository]: https://github.com/hecrj/iced +//! [web runtime]: https://github.com/iced-rs/iced_web +//! [examples]: https://github.com/iced-rs/iced/tree/0.4/examples +//! [repository]: https://github.com/iced-rs/iced //! //! # Overview //! Inspired by [The Elm Architecture], Iced expects you to split user @@ -51,15 +51,9 @@ //! We start by modelling the __state__ of our application: //! //! ``` -//! use iced::button; -//! //! struct Counter { //! // The counter value //! value: i32, -//! -//! // The local state of the two buttons -//! increment_button: button::State, -//! decrement_button: button::State, //! } //! ``` //! @@ -78,15 +72,9 @@ //! __view logic__: //! //! ``` -//! # use iced::button; -//! # //! # struct Counter { //! # // The counter value //! # value: i32, -//! # -//! # // The local state of the two buttons -//! # increment_button: button::State, -//! # decrement_button: button::State, //! # } //! # //! # #[derive(Debug, Clone, Copy)] @@ -95,28 +83,22 @@ //! # DecrementPressed, //! # } //! # -//! use iced::{Button, Column, Text}; +//! use iced::widget::{button, column, text, Column}; //! //! impl Counter { //! pub fn view(&mut self) -> Column<Message> { //! // We use a column: a simple vertical layout -//! Column::new() -//! .push( -//! // The increment button. We tell it to produce an -//! // `IncrementPressed` message when pressed -//! Button::new(&mut self.increment_button, Text::new("+")) -//! .on_press(Message::IncrementPressed), -//! ) -//! .push( -//! // We show the value of the counter here -//! Text::new(self.value.to_string()).size(50), -//! ) -//! .push( -//! // The decrement button. We tell it to produce a -//! // `DecrementPressed` message when pressed -//! Button::new(&mut self.decrement_button, Text::new("-")) -//! .on_press(Message::DecrementPressed), -//! ) +//! column![ +//! // The increment button. We tell it to produce an +//! // `IncrementPressed` message when pressed +//! button("+").on_press(Message::IncrementPressed), +//! +//! // We show the value of the counter here +//! text(self.value).size(50), +//! +//! // The decrement button. We tell it to produce a +//! button("-").on_press(Message::DecrementPressed), +//! ] //! } //! } //! ``` @@ -125,15 +107,9 @@ //! our __state__ accordingly in our __update logic__: //! //! ``` -//! # use iced::button; -//! # //! # struct Counter { //! # // The counter value //! # value: i32, -//! # -//! # // The local state of the two buttons -//! # increment_button: button::State, -//! # decrement_button: button::State, //! # } //! # //! # #[derive(Debug, Clone, Copy)] @@ -174,22 +150,31 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![forbid(unsafe_code)] -#![forbid(rust_2018_idioms)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(rust_2018_idioms, unsafe_code)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] -mod application; + mod element; mod error; mod result; mod sandbox; +pub mod application; pub mod clipboard; pub mod executor; pub mod keyboard; pub mod mouse; +pub mod overlay; pub mod settings; pub mod time; pub mod widget; @@ -207,20 +192,28 @@ use iced_wgpu as renderer; #[cfg(feature = "glow")] use iced_glow as renderer; -#[doc(no_inline)] -pub use widget::*; +pub use iced_native::theme; +pub use runtime::event; +pub use runtime::subscription; pub use application::Application; pub use element::Element; pub use error::Error; +pub use event::Event; pub use executor::Executor; +pub use renderer::Renderer; pub use result::Result; pub use sandbox::Sandbox; pub use settings::Settings; +pub use subscription::Subscription; +pub use theme::Theme; pub use runtime::alignment; pub use runtime::futures; pub use runtime::{ - Alignment, Background, Color, Command, ContentFit, Font, Length, Point, - Rectangle, Size, Subscription, Vector, + Alignment, Background, Color, Command, ContentFit, Font, Length, Padding, + Point, Rectangle, Size, Vector, }; + +#[cfg(feature = "system")] +pub use runtime::system; diff --git a/src/overlay.rs b/src/overlay.rs new file mode 100644 index 00000000..a7003751 --- /dev/null +++ b/src/overlay.rs @@ -0,0 +1,16 @@ +//! Display interactive elements on top of other widgets. + +/// A generic [`Overlay`]. +/// +/// This is an alias of an `iced_native` element with a default `Renderer`. +pub type Element<'a, Message, Renderer = crate::Renderer> = + iced_native::overlay::Element<'a, Message, Renderer>; + +pub mod menu { + //! Build and show dropdown menus. + pub use iced_native::overlay::menu::{Appearance, State, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Menu<'a, Message, Renderer = crate::Renderer> = + iced_native::overlay::Menu<'a, Message, Renderer>; +} diff --git a/src/sandbox.rs b/src/sandbox.rs index 2306c650..bdb6ad5a 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,6 +1,5 @@ -use crate::{ - Application, Color, Command, Element, Error, Settings, Subscription, -}; +use crate::theme::{self, Theme}; +use crate::{Application, Command, Element, Error, Settings, Subscription}; /// A sandboxed [`Application`]. /// @@ -35,19 +34,19 @@ use crate::{ /// - [`tour`], a simple UI tour that can run both on native platforms and the /// web! /// -/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples -/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool -/// [`counter`]: https://github.com/hecrj/iced/tree/0.3/examples/counter -/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget -/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry -/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid -/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.3/examples/progress_bar -/// [`styling`]: https://github.com/hecrj/iced/tree/0.3/examples/styling -/// [`svg`]: https://github.com/hecrj/iced/tree/0.3/examples/svg -/// [`tour`]: https://github.com/hecrj/iced/tree/0.3/examples/tour +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.4/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.4/examples/bezier_tool +/// [`counter`]: https://github.com/iced-rs/iced/tree/0.4/examples/counter +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.4/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.4/examples/geometry +/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid +/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.4/examples/progress_bar +/// [`styling`]: https://github.com/iced-rs/iced/tree/0.4/examples/styling +/// [`svg`]: https://github.com/iced-rs/iced/tree/0.4/examples/svg +/// [`tour`]: https://github.com/iced-rs/iced/tree/0.4/examples/tour /// [`Canvas widget`]: crate::widget::Canvas /// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.4/wgpu /// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// @@ -57,7 +56,7 @@ use crate::{ /// says "Hello, world!": /// /// ```no_run -/// use iced::{Element, Sandbox, Settings, Text}; +/// use iced::{Element, Sandbox, Settings}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -80,8 +79,8 @@ use crate::{ /// // This application has no interactions /// } /// -/// fn view(&mut self) -> Element<Self::Message> { -/// Text::new("Hello, world!").into() +/// fn view(&self) -> Element<Self::Message> { +/// "Hello, world!".into() /// } /// } /// ``` @@ -109,13 +108,23 @@ pub trait Sandbox { /// Returns the widgets to display in the [`Sandbox`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&mut self) -> Element<'_, Self::Message>; + fn view(&self) -> Element<'_, Self::Message>; - /// Returns the background color of the [`Sandbox`]. + /// Returns the current [`Theme`] of the [`Sandbox`]. /// - /// By default, it returns [`Color::WHITE`]. - fn background_color(&self) -> Color { - Color::WHITE + /// If you want to use your own custom theme type, you will have to use an + /// [`Application`]. + /// + /// By default, it returns [`Theme::default`]. + fn theme(&self) -> Theme { + Theme::default() + } + + /// Returns the current style variant of [`theme::Application`]. + /// + /// By default, it returns [`theme::Application::default`]. + fn style(&self) -> theme::Application { + theme::Application::default() } /// Returns the scale factor of the [`Sandbox`]. @@ -159,6 +168,7 @@ where type Executor = iced_futures::backend::null::Executor; type Flags = (); type Message = T::Message; + type Theme = Theme; fn new(_flags: ()) -> (Self, Command<T::Message>) { (T::new(), Command::none()) @@ -174,16 +184,20 @@ where Command::none() } - fn subscription(&self) -> Subscription<T::Message> { - Subscription::none() + fn view(&self) -> Element<'_, T::Message> { + T::view(self) } - fn view(&mut self) -> Element<'_, T::Message> { - T::view(self) + fn theme(&self) -> Self::Theme { + T::theme(self) + } + + fn style(&self) -> theme::Application { + T::style(self) } - fn background_color(&self) -> Color { - T::background_color(self) + fn subscription(&self) -> Subscription<T::Message> { + Subscription::none() } fn scale_factor(&self) -> f64 { diff --git a/src/widget.rs b/src/widget.rs index c619bcfa..9f09cb8f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,65 +1,232 @@ //! Display information and interactive controls in your application. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced::{button, Button}; -//! ``` -//! -//! # Stateful widgets -//! Some widgets need to keep track of __local state__. -//! -//! These widgets have their own module with a `State` type. For instance, a -//! [`TextInput`] has some [`text_input::State`]. -pub use crate::renderer::widget::{ - button, checkbox, container, pane_grid, pick_list, progress_bar, radio, - rule, scrollable, slider, text_input, toggler, tooltip, Column, Row, Space, - Text, -}; - -#[cfg(any(feature = "canvas", feature = "glow_canvas"))] -#[cfg_attr( - docsrs, - doc(cfg(any(feature = "canvas", feature = "glow_canvas"))) -)] -pub use crate::renderer::widget::canvas; - -#[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] -#[cfg_attr( - docsrs, - doc(cfg(any(feature = "qr_code", feature = "glow_qr_code"))) -)] -pub use crate::renderer::widget::qr_code; +pub use iced_native::widget::helpers::*; +pub use iced_native::{column, row}; + +/// A container that distributes its contents vertically. +pub type Column<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Column<'a, Message, Renderer>; + +/// A container that distributes its contents horizontally. +pub type Row<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Row<'a, Message, Renderer>; + +pub mod text { + //! Write some text for your users to read. + pub use iced_native::widget::text::{Appearance, StyleSheet}; + + /// A paragraph of text. + pub type Text<'a, Renderer = crate::Renderer> = + iced_native::widget::Text<'a, Renderer>; +} + +pub mod button { + //! Allow your users to perform actions by pressing a button. + pub use iced_native::widget::button::{Appearance, StyleSheet}; + + /// A widget that produces a message when clicked. + pub type Button<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Button<'a, Message, Renderer>; +} + +pub mod checkbox { + //! Show toggle controls using checkboxes. + pub use iced_native::widget::checkbox::{Appearance, StyleSheet}; + + /// A box that can be checked. + pub type Checkbox<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Checkbox<'a, Message, Renderer>; +} + +pub mod container { + //! Decorate content and apply alignment. + pub use iced_native::widget::container::{Appearance, StyleSheet}; + + /// An element decorating some content. + pub type Container<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Container<'a, Message, Renderer>; +} + +pub mod pane_grid { + //! Let your users split regions of your application and organize layout dynamically. + //! + //! [](https://gfycat.com/mixedflatjellyfish) + //! + //! # Example + //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, + //! drag and drop, and hotkey support. + //! + //! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.4/examples/pane_grid + pub use iced_native::widget::pane_grid::{ + Axis, Configuration, Direction, DragEvent, Line, Node, Pane, + ResizeEvent, Split, State, StyleSheet, + }; + + /// A collection of panes distributed using either vertical or horizontal splits + /// to completely fill the space available. + /// + /// [](https://gfycat.com/mixedflatjellyfish) + pub type PaneGrid<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::PaneGrid<'a, Message, Renderer>; + + /// The content of a [`Pane`]. + pub type Content<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::Content<'a, Message, Renderer>; + + /// The title bar of a [`Pane`]. + pub type TitleBar<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; +} + +pub mod pick_list { + //! Display a dropdown list of selectable values. + pub use iced_native::widget::pick_list::{Appearance, StyleSheet}; + + /// A widget allowing the selection of a single value from a list of options. + pub type PickList<'a, T, Message, Renderer = crate::Renderer> = + iced_native::widget::PickList<'a, T, Message, Renderer>; +} + +pub mod radio { + //! Create choices using radio buttons. + pub use iced_native::widget::radio::{Appearance, StyleSheet}; + + /// A circular button representing a choice. + pub type Radio<Message, Renderer = crate::Renderer> = + iced_native::widget::Radio<Message, Renderer>; +} + +pub mod scrollable { + //! Navigate an endless amount of content with a scrollbar. + pub use iced_native::widget::scrollable::{ + snap_to, style::Scrollbar, style::Scroller, Id, StyleSheet, + }; + + /// A widget that can vertically display an infinite amount of content + /// with a scrollbar. + pub type Scrollable<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Scrollable<'a, Message, Renderer>; +} + +pub mod toggler { + //! Show toggle controls using togglers. + pub use iced_native::widget::toggler::{Appearance, StyleSheet}; + + /// A toggler widget. + pub type Toggler<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Toggler<'a, Message, Renderer>; +} + +pub mod text_input { + //! Display fields that can be filled with text. + pub use iced_native::widget::text_input::{ + focus, Appearance, Id, StyleSheet, + }; + + /// A field that can be filled with text. + pub type TextInput<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::TextInput<'a, Message, Renderer>; +} + +pub mod tooltip { + //! Display a widget over another. + pub use iced_native::widget::tooltip::Position; + + /// A widget allowing the selection of a single value from a list of options. + pub type Tooltip<'a, Message, Renderer = crate::Renderer> = + iced_native::widget::Tooltip<'a, Message, Renderer>; +} + +pub use iced_native::widget::progress_bar; +pub use iced_native::widget::rule; +pub use iced_native::widget::slider; +pub use iced_native::widget::Space; + +pub use button::Button; +pub use checkbox::Checkbox; +pub use container::Container; +pub use pane_grid::PaneGrid; +pub use pick_list::PickList; +pub use progress_bar::ProgressBar; +pub use radio::Radio; +pub use rule::Rule; +pub use scrollable::Scrollable; +pub use slider::Slider; +pub use text::Text; +pub use text_input::TextInput; +pub use toggler::Toggler; +pub use tooltip::Tooltip; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub use iced_graphics::widget::canvas; + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +/// Creates a new [`Canvas`]. +pub fn canvas<P, Message, Theme>(program: P) -> Canvas<Message, Theme, P> +where + P: canvas::Program<Message, Theme>, +{ + Canvas::new(program) +} + +#[cfg(feature = "image")] #[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub mod image { //! Display images in your user interface. - pub use crate::runtime::image::Handle; - pub use crate::runtime::widget::image::viewer; - pub use crate::runtime::widget::image::{Image, Viewer}; + pub use iced_native::image::Handle; + + /// A frame that displays an image. + pub type Image = iced_native::widget::Image<Handle>; + + pub use iced_native::widget::image::viewer; + pub use viewer::Viewer; } +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub mod svg { - //! Display vector graphics in your user interface. - pub use crate::runtime::svg::Handle; - pub use crate::runtime::widget::svg::Svg; + //! Display vector graphics in your application. + pub use iced_native::svg::Handle; + pub use iced_native::widget::Svg; } -#[doc(no_inline)] -pub use { - button::Button, checkbox::Checkbox, container::Container, image::Image, - pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar, - radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider, svg::Svg, - text_input::TextInput, toggler::Toggler, tooltip::Tooltip, -}; - -#[cfg(any(feature = "canvas", feature = "glow_canvas"))] -#[doc(no_inline)] +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] pub use canvas::Canvas; -#[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] -#[doc(no_inline)] +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub use image::Image; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] pub use qr_code::QRCode; + +#[cfg(feature = "svg")] +#[cfg_attr(docsrs, doc(cfg(feature = "svg")))] +pub use svg::Svg; + +use crate::Command; +use iced_native::widget::operation; + +/// Focuses the previous focusable widget. +pub fn focus_previous<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_previous()) +} + +/// Focuses the next focusable widget. +pub fn focus_next<Message>() -> Command<Message> +where + Message: 'static, +{ + Command::widget(operation::focusable::focus_next()) +} diff --git a/src/window.rs b/src/window.rs index 71158816..eb5e17a6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,10 @@ //! Configure the window of your application in native platforms. -mod mode; mod position; mod settings; pub mod icon; pub use icon::Icon; -pub use mode::Mode; pub use position::Position; pub use settings::Settings; diff --git a/src/window/settings.rs b/src/window/settings.rs index 8e32f4fb..24d0f4f9 100644 --- a/src/window/settings.rs +++ b/src/window/settings.rs @@ -15,6 +15,9 @@ pub struct Settings { /// 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, @@ -38,6 +41,7 @@ impl Default for Settings { position: Position::default(), min_size: None, max_size: None, + visible: true, resizable: true, decorations: true, transparent: false, @@ -54,6 +58,7 @@ impl From<Settings> for iced_winit::settings::Window { 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, diff --git a/style/Cargo.toml b/style/Cargo.toml index 047c905d..cf9d328b 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_style" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "The default set of styles of Iced" @@ -11,5 +11,12 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [dependencies.iced_core] -version = "0.4" +version = "0.5" path = "../core" +features = ["palette"] + +[dependencies.palette] +version = "0.6" + +[dependencies.lazy_static] +version = "1.4" diff --git a/style/src/application.rs b/style/src/application.rs new file mode 100644 index 00000000..d48c6a34 --- /dev/null +++ b/style/src/application.rs @@ -0,0 +1,13 @@ +use iced_core::Color; + +pub trait StyleSheet { + type Style: Default + Copy; + + fn appearance(&self, style: Self::Style) -> Appearance; +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + pub background_color: Color, + pub text_color: Color, +} diff --git a/style/src/button.rs b/style/src/button.rs index de2de4f4..c63a6b71 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -3,7 +3,7 @@ use iced_core::{Background, Color, Vector}; /// The appearance of a button. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub shadow_offset: Vector, pub background: Option<Background>, pub border_radius: f32, @@ -12,7 +12,7 @@ pub struct Style { pub text_color: Color, } -impl std::default::Default for Style { +impl std::default::Default for Appearance { fn default() -> Self { Self { shadow_offset: Vector::default(), @@ -27,28 +27,30 @@ impl std::default::Default for Style { /// A set of rules that dictate the style of a button. pub trait StyleSheet { - fn active(&self) -> Style; + type Style: Default + Copy; - fn hovered(&self) -> Style { - let active = self.active(); + fn active(&self, style: Self::Style) -> Appearance; - Style { + fn hovered(&self, style: Self::Style) -> Appearance { + let active = self.active(style); + + Appearance { shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), ..active } } - fn pressed(&self) -> Style { - Style { + fn pressed(&self, style: Self::Style) -> Appearance { + Appearance { shadow_offset: Vector::default(), - ..self.active() + ..self.active(style) } } - fn disabled(&self) -> Style { - let active = self.active(); + fn disabled(&self, style: Self::Style) -> Appearance { + let active = self.active(style); - Style { + Appearance { shadow_offset: Vector::default(), background: active.background.map(|background| match background { Background::Color(color) => Background::Color(Color { @@ -64,33 +66,3 @@ pub trait StyleSheet { } } } - -struct Default; - -impl StyleSheet for Default { - fn active(&self) -> Style { - Style { - shadow_offset: Vector::new(0.0, 0.0), - background: Some(Background::Color([0.87, 0.87, 0.87].into())), - border_radius: 2.0, - border_width: 1.0, - border_color: [0.7, 0.7, 0.7].into(), - text_color: Color::BLACK, - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } -} diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs index de52e548..ba54b0a2 100644 --- a/style/src/checkbox.rs +++ b/style/src/checkbox.rs @@ -3,7 +3,7 @@ use iced_core::{Background, Color}; /// The appearance of a checkbox. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub background: Background, pub checkmark_color: Color, pub border_radius: f32, @@ -14,44 +14,9 @@ pub struct Style { /// A set of rules that dictate the style of a checkbox. pub trait StyleSheet { - fn active(&self, is_checked: bool) -> Style; + type Style: Default + Copy; - fn hovered(&self, is_checked: bool) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self, _is_checked: bool) -> Style { - Style { - background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)), - checkmark_color: Color::from_rgb(0.3, 0.3, 0.3), - border_radius: 5.0, - border_width: 1.0, - border_color: Color::from_rgb(0.6, 0.6, 0.6), - text_color: None, - } - } - - fn hovered(&self, is_checked: bool) -> Style { - Style { - background: Background::Color(Color::from_rgb(0.90, 0.90, 0.90)), - ..self.active(is_checked) - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} + fn active(&self, style: Self::Style, is_checked: bool) -> Appearance; -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } + fn hovered(&self, style: Self::Style, is_checked: bool) -> Appearance; } diff --git a/style/src/container.rs b/style/src/container.rs index 2f411611..184310fa 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -3,7 +3,7 @@ use iced_core::{Background, Color}; /// The appearance of a container. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub text_color: Option<Color>, pub background: Option<Background>, pub border_radius: f32, @@ -11,7 +11,7 @@ pub struct Style { pub border_color: Color, } -impl std::default::Default for Style { +impl std::default::Default for Appearance { fn default() -> Self { Self { text_color: None, @@ -23,37 +23,10 @@ impl std::default::Default for Style { } } -/// A set of rules that dictate the style of a container. +/// A set of rules that dictate the [`Appearance`] of a container. pub trait StyleSheet { - /// Produces the style of a container. - fn style(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn style(&self) -> Style { - Style { - text_color: None, - background: None, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} + type Style: Default + Copy; -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } + /// Produces the [`Appearance`] of a container. + fn appearance(&self, style: Self::Style) -> Appearance; } diff --git a/style/src/lib.rs b/style/src/lib.rs index e4556f67..0dde9582 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -7,8 +7,19 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] +#![deny( + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(unsafe_code, rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] pub use iced_core::{Background, Color}; +pub mod application; pub mod button; pub mod checkbox; pub mod container; @@ -20,5 +31,9 @@ pub mod radio; pub mod rule; pub mod scrollable; pub mod slider; +pub mod text; pub mod text_input; +pub mod theme; pub mod toggler; + +pub use theme::Theme; diff --git a/style/src/menu.rs b/style/src/menu.rs index 90985b8f..6ef3e2a2 100644 --- a/style/src/menu.rs +++ b/style/src/menu.rs @@ -2,24 +2,18 @@ use iced_core::{Background, Color}; /// The appearance of a menu. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub text_color: Color, pub background: Background, pub border_width: f32, + pub border_radius: f32, pub border_color: Color, pub selected_text_color: Color, pub selected_background: Background, } -impl std::default::Default for Style { - fn default() -> Self { - Self { - text_color: Color::BLACK, - background: Background::Color([0.87, 0.87, 0.87].into()), - border_width: 1.0, - border_color: [0.7, 0.7, 0.7].into(), - selected_text_color: Color::WHITE, - selected_background: Background::Color([0.4, 0.4, 1.0].into()), - } - } +pub trait StyleSheet { + type Style: Default + Copy; + + fn appearance(&self, style: Self::Style) -> Appearance; } diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs index a12ac3f5..5bae353f 100644 --- a/style/src/pane_grid.rs +++ b/style/src/pane_grid.rs @@ -4,11 +4,13 @@ use iced_core::Color; /// A set of rules that dictate the style of a container. pub trait StyleSheet { + type Style: Default + Copy; + /// The [`Line`] to draw when a split is picked. - fn picked_split(&self) -> Option<Line>; + fn picked_split(&self, style: Self::Style) -> Option<Line>; /// The [`Line`] to draw when a split is hovered. - fn hovered_split(&self) -> Option<Line>; + fn hovered_split(&self, style: Self::Style) -> Option<Line>; } /// A line. @@ -22,30 +24,3 @@ pub struct Line { /// The width of the [`Line`]. pub width: f32, } - -struct Default; - -impl StyleSheet for Default { - fn picked_split(&self) -> Option<Line> { - None - } - - fn hovered_split(&self) -> Option<Line> { - None - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } -} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs index ad96b201..2bafe932 100644 --- a/style/src/pick_list.rs +++ b/style/src/pick_list.rs @@ -1,9 +1,12 @@ -use crate::menu; use iced_core::{Background, Color}; +use crate::container; +use crate::menu; +use crate::scrollable; + /// The appearance of a pick list. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub text_color: Color, pub placeholder_color: Color, pub background: Background, @@ -13,60 +16,14 @@ pub struct Style { pub icon_size: f32, } -impl std::default::Default for Style { - fn default() -> Self { - Self { - text_color: Color::BLACK, - placeholder_color: [0.4, 0.4, 0.4].into(), - background: Background::Color([0.87, 0.87, 0.87].into()), - border_radius: 0.0, - border_width: 1.0, - border_color: [0.7, 0.7, 0.7].into(), - icon_size: 0.7, - } - } -} - /// A set of rules that dictate the style of a container. -pub trait StyleSheet { - fn menu(&self) -> menu::Style; +pub trait StyleSheet: + container::StyleSheet + menu::StyleSheet + scrollable::StyleSheet +{ + type Style: Default + Copy + Into<<Self as menu::StyleSheet>::Style>; - fn active(&self) -> Style; + fn active(&self, style: <Self as StyleSheet>::Style) -> Appearance; /// Produces the style of a container. - fn hovered(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn menu(&self) -> menu::Style { - menu::Style::default() - } - - fn active(&self) -> Style { - Style::default() - } - - fn hovered(&self) -> Style { - Style { - border_color: Color::BLACK, - ..self.active() - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: 'a + StyleSheet, -{ - fn from(style: T) -> Self { - Box::new(style) - } + fn hovered(&self, style: <Self as StyleSheet>::Style) -> Appearance; } diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs index a0195c7a..768e7c9c 100644 --- a/style/src/progress_bar.rs +++ b/style/src/progress_bar.rs @@ -1,9 +1,9 @@ //! Provide progress feedback to your users. -use iced_core::{Background, Color}; +use iced_core::Background; /// The appearance of a progress bar. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub background: Background, pub bar: Background, pub border_radius: f32, @@ -11,32 +11,7 @@ pub struct Style { /// A set of rules that dictate the style of a progress bar. pub trait StyleSheet { - fn style(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn style(&self) -> Style { - Style { - background: Background::Color(Color::from_rgb(0.6, 0.6, 0.6)), - bar: Background::Color(Color::from_rgb(0.3, 0.9, 0.3)), - border_radius: 5.0, - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} + type Style: Default + Copy; -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: 'a + StyleSheet, -{ - fn from(style: T) -> Self { - Box::new(style) - } + fn appearance(&self, style: Self::Style) -> Appearance; } diff --git a/style/src/radio.rs b/style/src/radio.rs index dab76ad8..a4d4a83b 100644 --- a/style/src/radio.rs +++ b/style/src/radio.rs @@ -3,7 +3,7 @@ use iced_core::{Background, Color}; /// The appearance of a radio button. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub background: Background, pub dot_color: Color, pub border_width: f32, @@ -13,43 +13,9 @@ pub struct Style { /// A set of rules that dictate the style of a radio button. pub trait StyleSheet { - fn active(&self) -> Style; + type Style: Default + Copy; - fn hovered(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self) -> Style { - Style { - background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)), - dot_color: Color::from_rgb(0.3, 0.3, 0.3), - border_width: 1.0, - border_color: Color::from_rgb(0.6, 0.6, 0.6), - text_color: None, - } - } - - fn hovered(&self) -> Style { - Style { - background: Background::Color(Color::from_rgb(0.90, 0.90, 0.90)), - ..self.active() - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} + fn active(&self, style: Self::Style) -> Appearance; -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } + fn hovered(&self, style: Self::Style) -> Appearance; } diff --git a/style/src/rule.rs b/style/src/rule.rs index 12a40f7d..af334912 100644 --- a/style/src/rule.rs +++ b/style/src/rule.rs @@ -1,6 +1,27 @@ //! Display a horizontal or vertical rule for dividing content. use iced_core::Color; +/// The appearance of a rule. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The color of the rule. + pub color: Color, + /// The width (thickness) of the rule line. + pub width: u16, + /// The radius of the line corners. + pub radius: f32, + /// The [`FillMode`] of the rule. + pub fill_mode: FillMode, +} + +/// A set of rules that dictate the style of a rule. +pub trait StyleSheet { + type Style: Default + Copy; + + /// Produces the style of a rule. + fn style(&self, style: Self::Style) -> Appearance; +} + /// The fill mode of a rule. #[derive(Debug, Clone, Copy)] pub enum FillMode { @@ -64,56 +85,3 @@ impl FillMode { } } } - -/// The appearance of a rule. -#[derive(Debug, Clone, Copy)] -pub struct Style { - /// The color of the rule. - pub color: Color, - /// The width (thickness) of the rule line. - pub width: u16, - /// The radius of the line corners. - pub radius: f32, - /// The [`FillMode`] of the rule. - pub fill_mode: FillMode, -} - -impl std::default::Default for Style { - fn default() -> Self { - Style { - color: [0.6, 0.6, 0.6, 0.6].into(), - width: 1, - radius: 0.0, - fill_mode: FillMode::Full, - } - } -} - -/// A set of rules that dictate the style of a rule. -pub trait StyleSheet { - /// Produces the style of a rule. - fn style(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn style(&self) -> Style { - Style::default() - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: 'a + StyleSheet, -{ - fn from(style: T) -> Self { - Box::new(style) - } -} diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs index 748ba888..8da7409c 100644 --- a/style/src/scrollable.rs +++ b/style/src/scrollable.rs @@ -22,55 +22,16 @@ pub struct Scroller { /// A set of rules that dictate the style of a scrollable. pub trait StyleSheet { + type Style: Default + Copy; + /// Produces the style of an active scrollbar. - fn active(&self) -> Scrollbar; + fn active(&self, style: Self::Style) -> Scrollbar; /// Produces the style of an hovered scrollbar. - fn hovered(&self) -> Scrollbar; + fn hovered(&self, style: Self::Style) -> Scrollbar; /// Produces the style of a scrollbar that is being dragged. - fn dragging(&self) -> Scrollbar { - self.hovered() - } -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self) -> Scrollbar { - Scrollbar { - background: None, - border_radius: 5.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: Scroller { - color: [0.0, 0.0, 0.0, 0.7].into(), - border_radius: 5.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> Scrollbar { - Scrollbar { - background: Some(Background::Color([0.0, 0.0, 0.0, 0.3].into())), - ..self.active() - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) + fn dragging(&self, style: Self::Style) -> Scrollbar { + self.hovered(style) } } diff --git a/style/src/slider.rs b/style/src/slider.rs index 1bb28b09..0ff0449b 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -3,7 +3,7 @@ use iced_core::Color; /// The appearance of a slider. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub rail_colors: (Color, Color), pub handle: Handle, } @@ -26,70 +26,14 @@ pub enum HandleShape { /// A set of rules that dictate the style of a slider. pub trait StyleSheet { + type Style: Default + Copy; + /// Produces the style of an active slider. - fn active(&self) -> Style; + fn active(&self, style: Self::Style) -> Appearance; /// Produces the style of an hovered slider. - fn hovered(&self) -> Style; + fn hovered(&self, style: Self::Style) -> Appearance; /// Produces the style of a slider that is being dragged. - fn dragging(&self) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self) -> Style { - Style { - rail_colors: ([0.6, 0.6, 0.6, 0.5].into(), Color::WHITE), - handle: Handle { - shape: HandleShape::Rectangle { - width: 8, - border_radius: 4.0, - }, - color: Color::from_rgb(0.95, 0.95, 0.95), - border_color: Color::from_rgb(0.6, 0.6, 0.6), - border_width: 1.0, - }, - } - } - - fn hovered(&self) -> Style { - let active = self.active(); - - Style { - handle: Handle { - color: Color::from_rgb(0.90, 0.90, 0.90), - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> Style { - let active = self.active(); - - Style { - handle: Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) - } + fn dragging(&self, style: Self::Style) -> Appearance; } diff --git a/style/src/text.rs b/style/src/text.rs new file mode 100644 index 00000000..6e3aeef8 --- /dev/null +++ b/style/src/text.rs @@ -0,0 +1,12 @@ +use iced_core::Color; + +pub trait StyleSheet { + type Style: Default + Copy; + + fn appearance(&self, style: Self::Style) -> Appearance; +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Appearance { + pub color: Option<Color>, +} diff --git a/style/src/text_input.rs b/style/src/text_input.rs index 3d5817cc..af86617b 100644 --- a/style/src/text_input.rs +++ b/style/src/text_input.rs @@ -3,87 +3,31 @@ use iced_core::{Background, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] -pub struct Style { +pub struct Appearance { pub background: Background, pub border_radius: f32, pub border_width: f32, pub border_color: Color, } -impl std::default::Default for Style { - fn default() -> Self { - Self { - background: Background::Color(Color::WHITE), - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } -} - /// A set of rules that dictate the style of a text input. pub trait StyleSheet { + type Style: Default + Copy; + /// Produces the style of an active text input. - fn active(&self) -> Style; + fn active(&self, style: Self::Style) -> Appearance; /// Produces the style of a focused text input. - fn focused(&self) -> Style; + fn focused(&self, style: Self::Style) -> Appearance; - fn placeholder_color(&self) -> Color; + fn placeholder_color(&self, style: Self::Style) -> Color; - fn value_color(&self) -> Color; + fn value_color(&self, style: Self::Style) -> Color; - fn selection_color(&self) -> Color; + fn selection_color(&self, style: Self::Style) -> Color; /// Produces the style of an hovered text input. - fn hovered(&self) -> Style { - self.focused() - } -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self) -> Style { - Style { - background: Background::Color(Color::WHITE), - border_radius: 5.0, - border_width: 1.0, - border_color: Color::from_rgb(0.7, 0.7, 0.7), - } - } - - fn focused(&self) -> Style { - Style { - border_color: Color::from_rgb(0.5, 0.5, 0.5), - ..self.active() - } - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.7, 0.7, 0.7) - } - - fn value_color(&self) -> Color { - Color::from_rgb(0.3, 0.3, 0.3) - } - - fn selection_color(&self) -> Color { - Color::from_rgb(0.8, 0.8, 1.0) - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} - -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: StyleSheet + 'a, -{ - fn from(style_sheet: T) -> Self { - Box::new(style_sheet) + fn hovered(&self, style: Self::Style) -> Appearance { + self.focused(style) } } diff --git a/style/src/theme.rs b/style/src/theme.rs new file mode 100644 index 00000000..9e9abfa0 --- /dev/null +++ b/style/src/theme.rs @@ -0,0 +1,719 @@ +pub mod palette; + +pub use self::palette::Palette; + +use crate::application; +use crate::button; +use crate::checkbox; +use crate::container; +use crate::menu; +use crate::pane_grid; +use crate::pick_list; +use crate::progress_bar; +use crate::radio; +use crate::rule; +use crate::scrollable; +use crate::slider; +use crate::text; +use crate::text_input; +use crate::toggler; + +use iced_core::{Background, Color}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + Light, + Dark, +} + +impl Theme { + pub fn palette(self) -> Palette { + match self { + Self::Light => Palette::LIGHT, + Self::Dark => Palette::DARK, + } + } + + pub fn extended_palette(&self) -> &palette::Extended { + match self { + Self::Light => &palette::EXTENDED_LIGHT, + Self::Dark => &palette::EXTENDED_DARK, + } + } +} + +impl Default for Theme { + fn default() -> Self { + Self::Light + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Application { + Default, + Custom(fn(Theme) -> application::Appearance), +} + +impl Default for Application { + fn default() -> Self { + Self::Default + } +} + +impl application::StyleSheet for Theme { + type Style = Application; + + fn appearance(&self, style: Self::Style) -> application::Appearance { + let palette = self.extended_palette(); + + match style { + Application::Default => application::Appearance { + background_color: palette.background.base.color, + text_color: palette.background.base.text, + }, + Application::Custom(f) => f(*self), + } + } +} + +/* + * Button + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Button { + Primary, + Secondary, + Positive, + Destructive, + Text, +} + +impl Default for Button { + fn default() -> Self { + Self::Primary + } +} + +impl button::StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: Self::Style) -> button::Appearance { + let palette = self.extended_palette(); + + let appearance = button::Appearance { + border_radius: 2.0, + ..button::Appearance::default() + }; + + let from_pair = |pair: palette::Pair| button::Appearance { + background: Some(pair.color.into()), + text_color: pair.text, + ..appearance + }; + + match style { + Button::Primary => from_pair(palette.primary.strong), + Button::Secondary => from_pair(palette.secondary.base), + Button::Positive => from_pair(palette.success.base), + Button::Destructive => from_pair(palette.danger.base), + Button::Text => button::Appearance { + text_color: palette.background.base.text, + ..appearance + }, + } + } + + fn hovered(&self, style: Self::Style) -> button::Appearance { + let active = self.active(style); + let palette = self.extended_palette(); + + let background = match style { + Button::Primary => Some(palette.primary.base.color), + Button::Secondary => Some(palette.background.strong.color), + Button::Positive => Some(palette.success.strong.color), + Button::Destructive => Some(palette.danger.strong.color), + Button::Text => None, + }; + + button::Appearance { + background: background.map(Background::from), + ..active + } + } +} + +/* + * Checkbox + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Checkbox { + Primary, + Secondary, + Success, + Danger, +} + +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} + +impl checkbox::StyleSheet for Theme { + type Style = Checkbox; + + fn active( + &self, + style: Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.base, + palette.primary.strong, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.base, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.base, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.base, + palette.danger.base, + is_checked, + ), + } + } + + fn hovered( + &self, + style: Self::Style, + is_checked: bool, + ) -> checkbox::Appearance { + let palette = self.extended_palette(); + + match style { + Checkbox::Primary => checkbox_appearance( + palette.primary.strong.text, + palette.background.weak, + palette.primary.base, + is_checked, + ), + Checkbox::Secondary => checkbox_appearance( + palette.background.base.text, + palette.background.weak, + palette.background.base, + is_checked, + ), + Checkbox::Success => checkbox_appearance( + palette.success.base.text, + palette.background.weak, + palette.success.base, + is_checked, + ), + Checkbox::Danger => checkbox_appearance( + palette.danger.base.text, + palette.background.weak, + palette.danger.base, + is_checked, + ), + } + } +} + +fn checkbox_appearance( + checkmark_color: Color, + base: palette::Pair, + accent: palette::Pair, + is_checked: bool, +) -> checkbox::Appearance { + checkbox::Appearance { + background: Background::Color(if is_checked { + accent.color + } else { + base.color + }), + checkmark_color, + border_radius: 2.0, + border_width: 1.0, + border_color: accent.color, + text_color: None, + } +} + +/* + * Container + */ +#[derive(Clone, Copy)] +pub enum Container { + Transparent, + Box, + Custom(fn(&Theme) -> container::Appearance), +} + +impl Default for Container { + fn default() -> Self { + Self::Transparent + } +} + +impl From<fn(&Theme) -> container::Appearance> for Container { + fn from(f: fn(&Theme) -> container::Appearance) -> Self { + Self::Custom(f) + } +} + +impl container::StyleSheet for Theme { + type Style = Container; + + fn appearance(&self, style: Self::Style) -> container::Appearance { + match style { + Container::Transparent => Default::default(), + Container::Box => { + let palette = self.extended_palette(); + + container::Appearance { + text_color: None, + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Custom(f) => f(self), + } + } +} + +/* + * Slider + */ +impl slider::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> slider::Appearance { + let palette = self.extended_palette(); + + let handle = slider::Handle { + shape: slider::HandleShape::Rectangle { + width: 8, + border_radius: 4.0, + }, + color: Color::WHITE, + border_color: Color::WHITE, + border_width: 1.0, + }; + + slider::Appearance { + rail_colors: (palette.primary.base.color, Color::TRANSPARENT), + handle: slider::Handle { + color: palette.background.base.color, + border_color: palette.primary.base.color, + ..handle + }, + } + } + + fn hovered(&self, style: Self::Style) -> slider::Appearance { + let active = self.active(style); + let palette = self.extended_palette(); + + slider::Appearance { + handle: slider::Handle { + color: palette.primary.weak.color, + ..active.handle + }, + ..active + } + } + + fn dragging(&self, style: Self::Style) -> slider::Appearance { + let active = self.active(style); + let palette = self.extended_palette(); + + slider::Appearance { + handle: slider::Handle { + color: palette.primary.base.color, + ..active.handle + }, + ..active + } + } +} + +/* + * Menu + */ +impl menu::StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: Self::Style) -> menu::Appearance { + let palette = self.extended_palette(); + + menu::Appearance { + text_color: palette.background.weak.text, + background: palette.background.weak.color.into(), + border_width: 1.0, + border_radius: 0.0, + border_color: palette.background.strong.color, + selected_text_color: palette.primary.strong.text, + selected_background: palette.primary.strong.color.into(), + } + } +} + +/* + * Pick List + */ +impl pick_list::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: ()) -> pick_list::Appearance { + let palette = self.extended_palette(); + + pick_list::Appearance { + text_color: palette.background.weak.text, + background: palette.background.weak.color.into(), + placeholder_color: palette.background.strong.color, + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.strong.color, + icon_size: 0.7, + } + } + + fn hovered(&self, _style: ()) -> pick_list::Appearance { + let palette = self.extended_palette(); + + pick_list::Appearance { + text_color: palette.background.weak.text, + background: palette.background.weak.color.into(), + placeholder_color: palette.background.strong.color, + border_radius: 2.0, + border_width: 1.0, + border_color: palette.primary.strong.color, + icon_size: 0.7, + } + } +} + +/* + * Radio + */ +impl radio::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> radio::Appearance { + let palette = self.extended_palette(); + + radio::Appearance { + background: Color::TRANSPARENT.into(), + dot_color: palette.primary.strong.color, + border_width: 1.0, + border_color: palette.primary.strong.color, + text_color: None, + } + } + + fn hovered(&self, style: Self::Style) -> radio::Appearance { + let active = self.active(style); + let palette = self.extended_palette(); + + radio::Appearance { + dot_color: palette.primary.strong.color, + background: palette.primary.weak.color.into(), + ..active + } + } +} + +/* + * Toggler + */ +impl toggler::StyleSheet for Theme { + type Style = (); + + fn active( + &self, + _style: Self::Style, + is_active: bool, + ) -> toggler::Appearance { + let palette = self.extended_palette(); + + toggler::Appearance { + background: if is_active { + palette.primary.strong.color + } else { + palette.background.strong.color + }, + background_border: None, + foreground: if is_active { + palette.primary.strong.text + } else { + palette.background.base.color + }, + foreground_border: None, + } + } + + fn hovered( + &self, + style: Self::Style, + is_active: bool, + ) -> toggler::Appearance { + let palette = self.extended_palette(); + + toggler::Appearance { + foreground: if is_active { + Color { + a: 0.5, + ..palette.primary.strong.text + } + } else { + palette.background.weak.color + }, + ..self.active(style, is_active) + } + } +} + +/* + * Pane Grid + */ +impl pane_grid::StyleSheet for Theme { + type Style = (); + + fn picked_split(&self, _style: Self::Style) -> Option<pane_grid::Line> { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.strong.color, + width: 2.0, + }) + } + + fn hovered_split(&self, _style: Self::Style) -> Option<pane_grid::Line> { + let palette = self.extended_palette(); + + Some(pane_grid::Line { + color: palette.primary.base.color, + width: 2.0, + }) + } +} + +/* + * Progress Bar + */ +#[derive(Clone, Copy)] +pub enum ProgressBar { + Primary, + Success, + Danger, + Custom(fn(&Theme) -> progress_bar::Appearance), +} + +impl Default for ProgressBar { + fn default() -> Self { + Self::Primary + } +} + +impl progress_bar::StyleSheet for Theme { + type Style = ProgressBar; + + fn appearance(&self, style: Self::Style) -> progress_bar::Appearance { + let palette = self.extended_palette(); + + let from_palette = |bar: Color| progress_bar::Appearance { + background: palette.background.strong.color.into(), + bar: bar.into(), + border_radius: 2.0, + }; + + match style { + ProgressBar::Primary => from_palette(palette.primary.base.color), + ProgressBar::Success => from_palette(palette.success.base.color), + ProgressBar::Danger => from_palette(palette.danger.base.color), + ProgressBar::Custom(f) => f(self), + } + } +} + +/* + * Rule + */ +#[derive(Clone, Copy)] +pub enum Rule { + Default, + Custom(fn(&Theme) -> rule::Appearance), +} + +impl Default for Rule { + fn default() -> Self { + Self::Default + } +} + +impl rule::StyleSheet for Theme { + type Style = Rule; + + fn style(&self, style: Self::Style) -> rule::Appearance { + let palette = self.extended_palette(); + + match style { + Rule::Default => rule::Appearance { + color: palette.background.strong.color, + width: 1, + radius: 0.0, + fill_mode: rule::FillMode::Full, + }, + Rule::Custom(f) => f(self), + } + } +} + +/* + * Scrollable + */ +impl scrollable::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.background.strong.color, + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self, _style: Self::Style) -> scrollable::Scrollbar { + let palette = self.extended_palette(); + + scrollable::Scrollbar { + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: palette.primary.strong.color, + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } +} + +/* + * Text + */ +#[derive(Clone, Copy)] +pub enum Text { + Default, + Color(Color), + Custom(fn(&Theme) -> text::Appearance), +} + +impl Default for Text { + fn default() -> Self { + Self::Default + } +} + +impl From<Color> for Text { + fn from(color: Color) -> Self { + Text::Color(color) + } +} + +impl text::StyleSheet for Theme { + type Style = Text; + + fn appearance(&self, style: Self::Style) -> text::Appearance { + match style { + Text::Default => Default::default(), + Text::Color(c) => text::Appearance { color: Some(c) }, + Text::Custom(f) => f(self), + } + } +} + +/* + * Text Input + */ +impl text_input::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, _style: Self::Style) -> text_input::Appearance { + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, _style: Self::Style) -> Color { + let palette = self.extended_palette(); + + palette.primary.weak.color + } +} diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs new file mode 100644 index 00000000..81aa9cc7 --- /dev/null +++ b/style/src/theme/palette.rs @@ -0,0 +1,277 @@ +use iced_core::Color; + +use lazy_static::lazy_static; +use palette::{FromColor, Hsl, Mix, RelativeContrast, Srgb}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Palette { + pub background: Color, + pub text: Color, + pub primary: Color, + pub success: Color, + pub danger: Color, +} + +impl Palette { + pub const LIGHT: Self = Self { + background: Color::WHITE, + text: Color::BLACK, + primary: Color::from_rgb( + 0x5E as f32 / 255.0, + 0x7C as f32 / 255.0, + 0xE2 as f32 / 255.0, + ), + success: Color::from_rgb( + 0x12 as f32 / 255.0, + 0x66 as f32 / 255.0, + 0x4F as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xC3 as f32 / 255.0, + 0x42 as f32 / 255.0, + 0x3F as f32 / 255.0, + ), + }; + + pub const DARK: Self = Self { + background: Color::from_rgb( + 0x20 as f32 / 255.0, + 0x22 as f32 / 255.0, + 0x25 as f32 / 255.0, + ), + text: Color::from_rgb(0.90, 0.90, 0.90), + primary: Color::from_rgb( + 0x5E as f32 / 255.0, + 0x7C as f32 / 255.0, + 0xE2 as f32 / 255.0, + ), + success: Color::from_rgb( + 0x12 as f32 / 255.0, + 0x66 as f32 / 255.0, + 0x4F as f32 / 255.0, + ), + danger: Color::from_rgb( + 0xC3 as f32 / 255.0, + 0x42 as f32 / 255.0, + 0x3F as f32 / 255.0, + ), + }; +} + +pub struct Extended { + pub background: Background, + pub primary: Primary, + pub secondary: Secondary, + pub success: Success, + pub danger: Danger, +} + +lazy_static! { + pub static ref EXTENDED_LIGHT: Extended = + Extended::generate(Palette::LIGHT); + pub static ref EXTENDED_DARK: Extended = Extended::generate(Palette::DARK); +} + +impl Extended { + pub fn generate(palette: Palette) -> Self { + Self { + background: Background::new(palette.background, palette.text), + primary: Primary::generate( + palette.primary, + palette.background, + palette.text, + ), + secondary: Secondary::generate(palette.background, palette.text), + success: Success::generate( + palette.success, + palette.background, + palette.text, + ), + danger: Danger::generate( + palette.danger, + palette.background, + palette.text, + ), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Pair { + pub color: Color, + pub text: Color, +} + +impl Pair { + pub fn new(color: Color, text: Color) -> Self { + Self { + color, + text: readable(color, text), + } + } +} + +pub struct Background { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Background { + pub fn new(base: Color, text: Color) -> Self { + let weak = mix(base, text, 0.15); + let strong = mix(base, text, 0.40); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Primary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Primary { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Secondary { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Secondary { + pub fn generate(base: Color, text: Color) -> Self { + let base = mix(base, text, 0.2); + let weak = mix(base, text, 0.1); + let strong = mix(base, text, 0.3); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Success { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Success { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +pub struct Danger { + pub base: Pair, + pub weak: Pair, + pub strong: Pair, +} + +impl Danger { + pub fn generate(base: Color, background: Color, text: Color) -> Self { + let weak = mix(base, background, 0.4); + let strong = deviate(base, 0.1); + + Self { + base: Pair::new(base, text), + weak: Pair::new(weak, text), + strong: Pair::new(strong, text), + } + } +} + +fn darken(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness - amount < 0.0 { + 0.0 + } else { + hsl.lightness - amount + }; + + from_hsl(hsl) +} + +fn lighten(color: Color, amount: f32) -> Color { + let mut hsl = to_hsl(color); + + hsl.lightness = if hsl.lightness + amount > 1.0 { + 1.0 + } else { + hsl.lightness + amount + }; + + from_hsl(hsl) +} + +fn deviate(color: Color, amount: f32) -> Color { + if is_dark(color) { + lighten(color, amount) + } else { + darken(color, amount) + } +} + +fn mix(a: Color, b: Color, factor: f32) -> Color { + let a_lin = Srgb::from(a).into_linear(); + let b_lin = Srgb::from(b).into_linear(); + + let mixed = a_lin.mix(&b_lin, factor); + Srgb::from_linear(mixed).into() +} + +fn readable(background: Color, text: Color) -> Color { + if is_readable(background, text) { + text + } else if is_dark(background) { + Color::WHITE + } else { + Color::BLACK + } +} + +fn is_dark(color: Color) -> bool { + to_hsl(color).lightness < 0.6 +} + +fn is_readable(a: Color, b: Color) -> bool { + let a_srgb = Srgb::from(a); + let b_srgb = Srgb::from(b); + + a_srgb.has_enhanced_contrast_text(&b_srgb) +} + +fn to_hsl(color: Color) -> Hsl { + Hsl::from_color(Srgb::from(color)) +} + +fn from_hsl(hsl: Hsl) -> Color { + Srgb::from_color(hsl).into() +} diff --git a/style/src/toggler.rs b/style/src/toggler.rs index c06a8cd1..4ee7db46 100644 --- a/style/src/toggler.rs +++ b/style/src/toggler.rs @@ -3,7 +3,7 @@ use iced_core::Color; /// The appearance of a toggler. #[derive(Debug)] -pub struct Style { +pub struct Appearance { pub background: Color, pub background_border: Option<Color>, pub foreground: Color, @@ -12,46 +12,9 @@ pub struct Style { /// A set of rules that dictate the style of a toggler. pub trait StyleSheet { - fn active(&self, is_active: bool) -> Style; + type Style: Default + Copy; - fn hovered(&self, is_active: bool) -> Style; -} - -struct Default; - -impl StyleSheet for Default { - fn active(&self, is_active: bool) -> Style { - Style { - background: if is_active { - Color::from_rgb(0.0, 1.0, 0.0) - } else { - Color::from_rgb(0.7, 0.7, 0.7) - }, - background_border: None, - foreground: Color::WHITE, - foreground_border: None, - } - } - - fn hovered(&self, is_active: bool) -> Style { - Style { - foreground: Color::from_rgb(0.95, 0.95, 0.95), - ..self.active(is_active) - } - } -} - -impl<'a> std::default::Default for Box<dyn StyleSheet + 'a> { - fn default() -> Self { - Box::new(Default) - } -} + fn active(&self, style: Self::Style, is_active: bool) -> Appearance; -impl<'a, T> From<T> for Box<dyn StyleSheet + 'a> -where - T: 'a + StyleSheet, -{ - fn from(style: T) -> Self { - Box::new(style) - } + fn hovered(&self, style: Self::Style, is_active: bool) -> Appearance; } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index b4173413..586f97d3 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_wgpu" -version = "0.4.0" +version = "0.5.1" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A wgpu renderer for Iced" @@ -28,8 +28,8 @@ spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] [dependencies] -wgpu = "0.12" -wgpu_glyph = "0.16" +wgpu = "0.13" +wgpu_glyph = "0.17" glyph_brush = "0.7" raw-window-handle = "0.4" log = "0.4" @@ -39,15 +39,15 @@ kamadak-exif = "0.5" bitflags = "1.2" [dependencies.bytemuck] -version = "1.4" +version = "1.9" features = ["derive"] [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" [dependencies.iced_graphics] -version = "0.2" +version = "0.3" path = "../graphics" features = ["font-fallback", "font-icons"] diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 05b4af9b..8c875254 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -93,7 +93,7 @@ impl Backend { &layer, staging_belt, encoder, - &frame, + frame, target_size.width, target_size.height, ); @@ -230,7 +230,6 @@ impl Backend { wgpu_glyph::VerticalAlign::Bottom } }), - ..Default::default() }; self.text_pipeline.queue(text); diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 0fefbfaf..d964aed7 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -136,7 +136,7 @@ impl Pipeline { }); let shader = - device.create_shader_module(&wgpu::ShaderModuleDescriptor { + device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("iced_wgpu::image::shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("shader/image.wgsl"), @@ -176,7 +176,7 @@ impl Pipeline { fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", - targets: &[wgpu::ColorTargetState { + targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { color: wgpu::BlendComponent { @@ -191,7 +191,7 @@ impl Pipeline { }, }), write_mask: wgpu::ColorWrites::ALL, - }], + })], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -236,7 +236,7 @@ impl Pipeline { entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView( - &texture_atlas.view(), + texture_atlas.view(), ), }], }); @@ -264,7 +264,7 @@ impl Pipeline { #[cfg(feature = "image_rs")] pub fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { let mut cache = self.raster_cache.borrow_mut(); - let memory = cache.load(&handle); + let memory = cache.load(handle); memory.dimensions() } @@ -272,7 +272,7 @@ impl Pipeline { #[cfg(feature = "svg")] pub fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32) { let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(&handle); + let svg = cache.load(handle); svg.viewport_dimensions() } @@ -358,7 +358,7 @@ impl Pipeline { entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView( - &self.texture_atlas.view(), + self.texture_atlas.view(), ), }], }); @@ -406,14 +406,16 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::image render pass"), - color_attachments: &[wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, }, - }], + )], depth_stencil_attachment: None, }); diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index c1347e55..953dd4e2 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -113,13 +113,7 @@ impl Atlas { match &entry { Entry::Contiguous(allocation) => { self.upload_allocation( - &buffer, - width, - height, - padding, - 0, - &allocation, - encoder, + &buffer, width, height, padding, 0, allocation, encoder, ); } Entry::Fragmented { fragments, .. } => { diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs index b1084ed9..cf089601 100644 --- a/wgpu/src/image/atlas/layer.rs +++ b/wgpu/src/image/atlas/layer.rs @@ -9,9 +9,6 @@ pub enum Layer { impl Layer { pub fn is_empty(&self) -> bool { - match self { - Layer::Empty => true, - _ => false, - } + matches!(self, Layer::Empty) } } diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index ec5e911f..2b4d4af3 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -59,7 +59,7 @@ impl Cache { } } image::Data::Bytes(bytes) => { - if let Ok(image) = image_rs::load_from_memory(&bytes) { + if let Ok(image) = image_rs::load_from_memory(bytes) { let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) .ok() @@ -103,7 +103,7 @@ impl Cache { if let Memory::Host(image) = memory { let (width, height) = image.dimensions(); - let entry = atlas.upload(width, height, &image, device, encoder)?; + let entry = atlas.upload(width, height, image, device, encoder)?; *memory = Memory::Device(entry); } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 4c830913..b08a0aa2 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -60,7 +60,7 @@ impl Cache { } svg::Data::Bytes(bytes) => { match usvg::Tree::from_data( - &bytes, + bytes, &usvg::Options::default().to_ref(), ) { Ok(tree) => Svg::Loaded(tree), @@ -112,7 +112,7 @@ impl Cache { // It would be cool to be able to smooth resize the `svg` example. let mut img = tiny_skia::Pixmap::new(width, height)?; - let _ = resvg::render( + resvg::render( tree, if width > height { usvg::FitTo::Width(width) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index fb03854b..3a98c6bd 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -16,23 +16,30 @@ //! - Meshes of triangles, useful to draw geometry freely. //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/master/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs //! [WebGPU API]: https://gpuweb.github.io/gpuweb/ //! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![deny(unsafe_code)] +#![deny( + missing_debug_implementations, + missing_docs, + unsafe_code, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] #![forbid(rust_2018_idioms)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] pub mod settings; pub mod triangle; -pub mod widget; pub mod window; mod backend; @@ -40,14 +47,12 @@ mod quad; mod text; pub use iced_graphics::{Antialiasing, Color, Error, Primitive, Viewport}; +pub use iced_native::Theme; pub use wgpu; pub use backend::Backend; pub use settings::Settings; -#[doc(no_inline)] -pub use widget::*; - pub(crate) use iced_graphics::Transformation; #[cfg(any(feature = "image_rs", feature = "svg"))] @@ -57,4 +62,5 @@ mod image; /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +pub type Renderer<Theme = iced_native::Theme> = + iced_graphics::Renderer<Backend, Theme>; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 22f3b815..a117df64 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -59,7 +59,7 @@ impl Pipeline { }); let shader = - device.create_shader_module(&wgpu::ShaderModuleDescriptor { + device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("iced_wgpu::quad::shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("shader/quad.wgsl"), @@ -100,7 +100,7 @@ impl Pipeline { fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", - targets: &[wgpu::ColorTargetState { + targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { color: wgpu::BlendComponent { @@ -115,7 +115,7 @@ impl Pipeline { }, }), write_mask: wgpu::ColorWrites::ALL, - }], + })], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -211,14 +211,16 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::quad render pass"), - color_attachments: &[wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, }, - }], + )], depth_stencil_attachment: None, }); diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 23b55904..7bc752ff 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -63,7 +63,7 @@ impl Settings { impl Default for Settings { fn default() -> Settings { Settings { - present_mode: wgpu::PresentMode::Mailbox, + present_mode: wgpu::PresentMode::AutoVsync, internal_backend: wgpu::Backends::all(), default_font: None, default_text_size: 20, diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl index f8f6e2d4..c2ea223f 100644 --- a/wgpu/src/shader/blit.wgsl +++ b/wgpu/src/shader/blit.wgsl @@ -16,19 +16,19 @@ var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>( vec2<f32>(1.0, 1.0) ); -[[group(0), binding(0)]] var u_sampler: sampler; -[[group(1), binding(0)]] var u_texture: texture_2d<f32>; +@group(0) @binding(0) var u_sampler: sampler; +@group(1) @binding(0) var u_texture: texture_2d<f32>; struct VertexInput { - [[builtin(vertex_index)]] vertex_index: u32; -}; + @builtin(vertex_index) vertex_index: u32, +} struct VertexOutput { - [[builtin(position)]] position: vec4<f32>; - [[location(0)]] uv: vec2<f32>; -}; + @builtin(position) position: vec4<f32>, + @location(0) uv: vec2<f32>, +} -[[stage(vertex)]] +@vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; out.uv = uvs[input.vertex_index]; @@ -37,7 +37,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { return out; } -[[stage(fragment)]] -fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return textureSample(u_texture, u_sampler, input.uv); } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index ff304422..5e22cdf4 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -1,27 +1,27 @@ struct Globals { - transform: mat4x4<f32>; -}; + transform: mat4x4<f32>, +} -[[group(0), binding(0)]] var<uniform> globals: Globals; -[[group(0), binding(1)]] var u_sampler: sampler; -[[group(1), binding(0)]] var u_texture: texture_2d_array<f32>; +@group(0) @binding(0) var<uniform> globals: Globals; +@group(0) @binding(1) var u_sampler: sampler; +@group(1) @binding(0) var u_texture: texture_2d_array<f32>; struct VertexInput { - [[location(0)]] v_pos: vec2<f32>; - [[location(1)]] pos: vec2<f32>; - [[location(2)]] scale: vec2<f32>; - [[location(3)]] atlas_pos: vec2<f32>; - [[location(4)]] atlas_scale: vec2<f32>; - [[location(5)]] layer: i32; -}; + @location(0) v_pos: vec2<f32>, + @location(1) pos: vec2<f32>, + @location(2) scale: vec2<f32>, + @location(3) atlas_pos: vec2<f32>, + @location(4) atlas_scale: vec2<f32>, + @location(5) layer: i32, +} struct VertexOutput { - [[builtin(position)]] position: vec4<f32>; - [[location(0)]] uv: vec2<f32>; - [[location(1)]] layer: f32; // this should be an i32, but naga currently reads that as requiring interpolation. -}; + @builtin(position) position: vec4<f32>, + @location(0) uv: vec2<f32>, + @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. +} -[[stage(vertex)]] +@vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; @@ -40,7 +40,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { return out; } -[[stage(fragment)]] -fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); } diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index 73f5d597..73edd97c 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -1,31 +1,31 @@ struct Globals { - transform: mat4x4<f32>; - scale: f32; -}; + transform: mat4x4<f32>, + scale: f32, +} -[[group(0), binding(0)]] var<uniform> globals: Globals; +@group(0) @binding(0) var<uniform> globals: Globals; struct VertexInput { - [[location(0)]] v_pos: vec2<f32>; - [[location(1)]] pos: vec2<f32>; - [[location(2)]] scale: vec2<f32>; - [[location(3)]] color: vec4<f32>; - [[location(4)]] border_color: vec4<f32>; - [[location(5)]] border_radius: f32; - [[location(6)]] border_width: f32; -}; + @location(0) v_pos: vec2<f32>, + @location(1) pos: vec2<f32>, + @location(2) scale: vec2<f32>, + @location(3) color: vec4<f32>, + @location(4) border_color: vec4<f32>, + @location(5) border_radius: f32, + @location(6) border_width: f32, +} struct VertexOutput { - [[builtin(position)]] position: vec4<f32>; - [[location(0)]] color: vec4<f32>; - [[location(1)]] border_color: vec4<f32>; - [[location(2)]] pos: vec2<f32>; - [[location(3)]] scale: vec2<f32>; - [[location(4)]] border_radius: f32; - [[location(5)]] border_width: f32; -}; - -[[stage(vertex)]] + @builtin(position) position: vec4<f32>, + @location(0) color: vec4<f32>, + @location(1) border_color: vec4<f32>, + @location(2) pos: vec2<f32>, + @location(3) scale: vec2<f32>, + @location(4) border_radius: f32, + @location(5) border_width: f32, +} + +@vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; @@ -77,10 +77,10 @@ fn distance_alg( } -[[stage(fragment)]] +@fragment fn fs_main( input: VertexOutput -) -> [[location(0)]] vec4<f32> { +) -> @location(0) vec4<f32> { var mixed_color: vec4<f32> = input.color; if (input.border_width > 0.0) { @@ -96,7 +96,7 @@ fn fs_main( internal_border ); - var border_mix: f32 = smoothStep( + var border_mix: f32 = smoothstep( max(internal_border - 0.5, 0.0), internal_border + 0.5, internal_distance @@ -112,7 +112,7 @@ fn fs_main( input.border_radius ); - var radius_alpha: f32 = 1.0 - smoothStep( + var radius_alpha: f32 = 1.0 - smoothstep( max(input.border_radius - 0.5, 0.0), input.border_radius + 0.5, dist); diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl index 61d9c5a4..b24402f8 100644 --- a/wgpu/src/shader/triangle.wgsl +++ b/wgpu/src/shader/triangle.wgsl @@ -1,20 +1,20 @@ struct Globals { - transform: mat4x4<f32>; -}; + transform: mat4x4<f32>, +} -[[group(0), binding(0)]] var<uniform> globals: Globals; +@group(0) @binding(0) var<uniform> globals: Globals; struct VertexInput { - [[location(0)]] position: vec2<f32>; - [[location(1)]] color: vec4<f32>; -}; + @location(0) position: vec2<f32>, + @location(1) color: vec4<f32>, +} struct VertexOutput { - [[builtin(position)]] position: vec4<f32>; - [[location(0)]] color: vec4<f32>; -}; + @builtin(position) position: vec4<f32>, + @location(0) color: vec4<f32>, +} -[[stage(vertex)]] +@vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; @@ -24,7 +24,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { return out; } -[[stage(fragment)]] -fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> { +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return input.color; } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 45f1f2de..e17b84c1 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -188,7 +188,8 @@ impl Pipeline { } b_count += utf8_len; } - return byte_index; + + byte_index }; if !nearest_only { diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index c702243b..fd06dddf 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -132,7 +132,7 @@ impl Pipeline { }); let shader = - device.create_shader_module(&wgpu::ShaderModuleDescriptor { + device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("iced_wgpu::triangle::shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("shader/triangle.wgsl"), @@ -160,22 +160,11 @@ impl Pipeline { fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", - targets: &[wgpu::ColorTargetState { + 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::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), + blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, - }], + })], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -184,9 +173,7 @@ impl Pipeline { }, depth_stencil: None, multisample: wgpu::MultisampleState { - count: u32::from( - antialiasing.map(|a| a.sample_count()).unwrap_or(1), - ), + count: antialiasing.map(|a| a.sample_count()).unwrap_or(1), mask: !0, alpha_to_coverage_enabled: false, }, @@ -283,47 +270,43 @@ impl Pipeline { let vertices = bytemuck::cast_slice(&mesh.buffers.vertices); let indices = bytemuck::cast_slice(&mesh.buffers.indices); - match ( + if let (Some(vertices_size), Some(indices_size)) = ( wgpu::BufferSize::new(vertices.len() as u64), wgpu::BufferSize::new(indices.len() as u64), ) { - (Some(vertices_size), Some(indices_size)) => { - { - let mut vertex_buffer = staging_belt.write_buffer( - encoder, - &self.vertex_buffer.raw, - (std::mem::size_of::<Vertex2D>() * last_vertex) - as u64, - vertices_size, - device, - ); - - vertex_buffer.copy_from_slice(vertices); - } - - { - let mut index_buffer = staging_belt.write_buffer( - encoder, - &self.index_buffer.raw, - (std::mem::size_of::<u32>() * last_index) as u64, - indices_size, - device, - ); - - index_buffer.copy_from_slice(indices); - } - - uniforms.push(transform); - offsets.push(( - last_vertex as u64, - last_index as u64, - mesh.buffers.indices.len(), - )); - - last_vertex += mesh.buffers.vertices.len(); - last_index += mesh.buffers.indices.len(); + { + let mut vertex_buffer = staging_belt.write_buffer( + encoder, + &self.vertex_buffer.raw, + (std::mem::size_of::<Vertex2D>() * last_vertex) as u64, + vertices_size, + device, + ); + + vertex_buffer.copy_from_slice(vertices); } - _ => {} + + { + let mut index_buffer = staging_belt.write_buffer( + encoder, + &self.index_buffer.raw, + (std::mem::size_of::<u32>() * last_index) as u64, + indices_size, + device, + ); + + index_buffer.copy_from_slice(indices); + } + + uniforms.push(transform); + offsets.push(( + last_vertex as u64, + last_index as u64, + mesh.buffers.indices.len(), + )); + + last_vertex += mesh.buffers.vertices.len(); + last_index += mesh.buffers.indices.len(); } } @@ -361,11 +344,13 @@ impl Pipeline { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::triangle render pass"), - color_attachments: &[wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { load, store: true }, - }], + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { load, store: true }, + }, + )], depth_stencil_attachment: None, }); diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 9fb87544..a3016ff8 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -74,7 +74,7 @@ impl Blit { }); let shader = - device.create_shader_module(&wgpu::ShaderModuleDescriptor { + device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("iced_wgpu::triangle::blit_shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("../shader/blit.wgsl"), @@ -93,22 +93,13 @@ impl Blit { fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", - targets: &[wgpu::ColorTargetState { + 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::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), + blend: Some( + wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, + ), write_mask: wgpu::ColorWrites::ALL, - }], + })], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -143,7 +134,7 @@ impl Blit { match &mut self.targets { None => { self.targets = Some(Targets::new( - &device, + device, self.format, &self.texture_layout, self.sample_count, @@ -154,7 +145,7 @@ impl Blit { Some(targets) => { if targets.width != width || targets.height != height { self.targets = Some(Targets::new( - &device, + device, self.format, &self.texture_layout, self.sample_count, @@ -178,14 +169,14 @@ impl Blit { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("iced_wgpu::triangle::msaa render pass"), - color_attachments: &[wgpu::RenderPassColorAttachment { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: true, }, - }], + })], depth_stencil_attachment: None, }); diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs deleted file mode 100644 index 99ae0ac2..00000000 --- a/wgpu/src/widget.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Use the widgets supported out-of-the-box. -//! -//! # Re-exports -//! For convenience, the contents of this module are available at the root -//! module. Therefore, you can directly type: -//! -//! ``` -//! use iced_wgpu::{button, Button}; -//! ``` -use crate::Renderer; - -pub mod button; -pub mod checkbox; -pub mod container; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod radio; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod text_input; -pub mod toggler; -pub mod tooltip; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -pub use container::Container; -#[doc(no_inline)] -pub use pane_grid::PaneGrid; -#[doc(no_inline)] -pub use pick_list::PickList; -#[doc(no_inline)] -pub use progress_bar::ProgressBar; -#[doc(no_inline)] -pub use radio::Radio; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - -pub use iced_native::widget::Space; - -/// A container that distributes its contents vertically. -pub type Column<'a, Message> = - iced_native::widget::Column<'a, Message, Renderer>; - -/// A container that distributes its contents horizontally. -pub type Row<'a, Message> = iced_native::widget::Row<'a, Message, Renderer>; - -/// A paragraph of text. -pub type Text = iced_native::widget::Text<Renderer>; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs deleted file mode 100644 index f11ff25e..00000000 --- a/wgpu/src/widget/button.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::button::{Style, StyleSheet}; -pub use iced_native::widget::button::State; - -/// A widget that produces a message when clicked. -/// -/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. -pub type Button<'a, Message> = - iced_native::widget::Button<'a, Message, Renderer>; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs deleted file mode 100644 index 399dd19c..00000000 --- a/wgpu/src/widget/canvas.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub use iced_graphics::canvas::*; diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs deleted file mode 100644 index 76d572d9..00000000 --- a/wgpu/src/widget/checkbox.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::Renderer; - -pub use iced_graphics::checkbox::{Style, StyleSheet}; - -/// A box that can be checked. -/// -/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. -pub type Checkbox<'a, Message> = - iced_native::widget::Checkbox<'a, Message, Renderer>; diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs deleted file mode 100644 index c16db50d..00000000 --- a/wgpu/src/widget/container.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Decorate content and apply alignment. -use crate::Renderer; - -pub use iced_graphics::container::{Style, StyleSheet}; - -/// An element decorating some content. -/// -/// This is an alias of an `iced_native` container with a default -/// `Renderer`. -pub type Container<'a, Message> = - iced_native::widget::Container<'a, Message, Renderer>; diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs deleted file mode 100644 index 38bdb672..00000000 --- a/wgpu/src/widget/pane_grid.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.3/examples/pane_grid -use crate::Renderer; - -pub use iced_graphics::pane_grid::{ - Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent, - Split, State, StyleSheet, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [](https://gfycat.com/mixedflatjellyfish) -/// -/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`. -pub type PaneGrid<'a, Message> = - iced_native::widget::PaneGrid<'a, Message, Renderer>; - -/// The content of a [`Pane`]. -pub type Content<'a, Message> = - iced_native::widget::pane_grid::Content<'a, Message, Renderer>; - -/// The title bar of a [`Pane`]. -pub type TitleBar<'a, Message> = - iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>; diff --git a/wgpu/src/widget/pick_list.rs b/wgpu/src/widget/pick_list.rs deleted file mode 100644 index 4d93be68..00000000 --- a/wgpu/src/widget/pick_list.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Display a dropdown list of selectable values. -pub use iced_native::widget::pick_list::State; - -pub use iced_graphics::overlay::menu::Style as Menu; -pub use iced_graphics::pick_list::{Style, StyleSheet}; - -/// A widget allowing the selection of a single value from a list of options. -pub type PickList<'a, T, Message> = - iced_native::widget::PickList<'a, T, Message, crate::Renderer>; diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs deleted file mode 100644 index 88391ccb..00000000 --- a/wgpu/src/widget/progress_bar.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Allow your users to visually track the progress of a computation. -//! -//! A [`ProgressBar`] has a range of possible values and a current value, -//! as well as a length, height and style. -pub use iced_graphics::progress_bar::*; diff --git a/wgpu/src/widget/qr_code.rs b/wgpu/src/widget/qr_code.rs deleted file mode 100644 index 7b1c2408..00000000 --- a/wgpu/src/widget/qr_code.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Encode and display information in a QR code. -pub use iced_graphics::qr_code::*; diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs deleted file mode 100644 index 9ef1d7a5..00000000 --- a/wgpu/src/widget/radio.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Create choices using radio buttons. -use crate::Renderer; - -pub use iced_graphics::radio::{Style, StyleSheet}; - -/// A circular button representing a choice. -/// -/// This is an alias of an `iced_native` radio button with an -/// `iced_wgpu::Renderer`. -pub type Radio<'a, Message> = iced_native::widget::Radio<'a, Message, Renderer>; diff --git a/wgpu/src/widget/rule.rs b/wgpu/src/widget/rule.rs deleted file mode 100644 index 40281773..00000000 --- a/wgpu/src/widget/rule.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. - -pub use iced_graphics::rule::*; diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs deleted file mode 100644 index d5635ec5..00000000 --- a/wgpu/src/widget/scrollable.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::Renderer; - -pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet}; -pub use iced_native::widget::scrollable::State; - -/// A widget that can vertically display an infinite amount of content -/// with a scrollbar. -/// -/// This is an alias of an `iced_native` scrollable with a default -/// `Renderer`. -pub type Scrollable<'a, Message> = - iced_native::widget::Scrollable<'a, Message, Renderer>; diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs deleted file mode 100644 index 2fb3d5d9..00000000 --- a/wgpu/src/widget/slider.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet}; -pub use iced_native::widget::slider::{Slider, State}; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs deleted file mode 100644 index 5560e3e0..00000000 --- a/wgpu/src/widget/text_input.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -use crate::Renderer; - -pub use iced_graphics::text_input::{Style, StyleSheet}; -pub use iced_native::widget::text_input::State; - -/// A field that can be filled with text. -/// -/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. -pub type TextInput<'a, Message> = - iced_native::widget::TextInput<'a, Message, Renderer>; diff --git a/wgpu/src/widget/toggler.rs b/wgpu/src/widget/toggler.rs deleted file mode 100644 index 7ef5e22e..00000000 --- a/wgpu/src/widget/toggler.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Show toggle controls using togglers. -use crate::Renderer; - -pub use iced_graphics::toggler::{Style, StyleSheet}; - -/// A toggler that can be toggled -/// -/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`. -pub type Toggler<'a, Message> = - iced_native::widget::Toggler<'a, Message, Renderer>; diff --git a/wgpu/src/widget/tooltip.rs b/wgpu/src/widget/tooltip.rs deleted file mode 100644 index c6af3903..00000000 --- a/wgpu/src/widget/tooltip.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Display a widget over another. -/// A widget allowing the selection of a single value from a list of options. -pub type Tooltip<'a, Message> = - iced_native::widget::Tooltip<'a, Message, crate::Renderer>; - -pub use iced_native::widget::tooltip::Position; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 6feb795b..a36d2a87 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,22 +1,27 @@ use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; -use futures::task::SpawnExt; +use futures::stream::{self, StreamExt}; + +use iced_graphics::compositor; use iced_native::futures; use raw_window_handle::HasRawWindowHandle; +use std::marker::PhantomData; + /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] -pub struct Compositor { +pub struct Compositor<Theme> { settings: Settings, instance: wgpu::Instance, + adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, staging_belt: wgpu::util::StagingBelt, - local_pool: futures::executor::LocalPool, format: wgpu::TextureFormat, + theme: PhantomData<Theme>, } -impl Compositor { +impl<Theme> Compositor<Theme> { const CHUNK_SIZE: u64 = 10 * 1024; /// Requests a new [`Compositor`] with the given [`Settings`]. @@ -28,6 +33,17 @@ impl Compositor { ) -> Option<Self> { let instance = wgpu::Instance::new(settings.internal_backend); + log::info!("{:#?}", settings); + + #[cfg(not(target_arch = "wasm32"))] + if log::max_level() >= log::LevelFilter::Info { + let available_adapters: Vec<_> = instance + .enumerate_adapters(settings.internal_backend) + .map(|adapter| adapter.get_info()) + .collect(); + log::info!("Available adapters: {:#?}", available_adapters); + } + #[allow(unsafe_code)] let compatible_surface = compatible_window .map(|window| unsafe { instance.create_surface(window) }); @@ -44,45 +60,55 @@ impl Compositor { }) .await?; - let format = compatible_surface - .as_ref() - .and_then(|surface| surface.get_preferred_format(&adapter))?; + log::info!("Selected: {:#?}", adapter.get_info()); + + let format = compatible_surface.as_ref().and_then(|surface| { + surface.get_supported_formats(&adapter).first().copied() + })?; + + log::info!("Selected format: {:?}", format); #[cfg(target_arch = "wasm32")] - let limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); + let limits = [wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits())]; #[cfg(not(target_arch = "wasm32"))] - let limits = wgpu::Limits::default(); - - let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - label: Some( - "iced_wgpu::window::compositor device descriptor", - ), - features: wgpu::Features::empty(), - limits: wgpu::Limits { - max_bind_groups: 2, - ..limits + let limits = + [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; + + let limits = limits.into_iter().map(|limits| wgpu::Limits { + max_bind_groups: 2, + ..limits + }); + + let (device, queue) = stream::iter(limits) + .filter_map(|limits| async { + adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some( + "iced_wgpu::window::compositor device descriptor", + ), + features: wgpu::Features::empty(), + limits, }, - }, - None, - ) - .await - .ok()?; + None, + ).await.ok() + }) + .boxed() + .next() + .await?; let staging_belt = wgpu::util::StagingBelt::new(Self::CHUNK_SIZE); - let local_pool = futures::executor::LocalPool::new(); Some(Compositor { instance, settings, + adapter, device, queue, staging_belt, - local_pool, format, + theme: PhantomData, }) } @@ -92,20 +118,20 @@ impl Compositor { } } -impl iced_graphics::window::Compositor for Compositor { +impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> { type Settings = Settings; - type Renderer = Renderer; + type Renderer = Renderer<Theme>; type Surface = wgpu::Surface; fn new<W: HasRawWindowHandle>( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Renderer), Error> { + ) -> Result<(Self, Self::Renderer), Error> { let compositor = futures::executor::block_on(Self::request( settings, compatible_window, )) - .ok_or(Error::AdapterNotFound)?; + .ok_or(Error::GraphicsAdapterNotFound)?; let backend = compositor.create_backend(); @@ -140,6 +166,15 @@ impl iced_graphics::window::Compositor for Compositor { ); } + fn fetch_information(&self) -> compositor::Information { + let information = self.adapter.get_info(); + + compositor::Information { + adapter: information.name, + backend: format!("{:?}", information.backend), + } + } + fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, @@ -147,7 +182,7 @@ impl iced_graphics::window::Compositor for Compositor { viewport: &Viewport, background_color: Color, overlay: &[T], - ) -> Result<(), iced_graphics::window::SurfaceError> { + ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { Ok(frame) => { let mut encoder = self.device.create_command_encoder( @@ -165,30 +200,32 @@ impl iced_graphics::window::Compositor for Compositor { label: Some( "iced_wgpu::window::Compositor render pass", ), - color_attachments: &[wgpu::RenderPassColorAttachment { - view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear({ - let [r, g, b, a] = - background_color.into_linear(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - store: true, + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear({ + let [r, g, b, a] = + background_color.into_linear(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + store: true, + }, }, - }], + )], depth_stencil_attachment: None, }); renderer.with_primitives(|backend, primitives| { backend.present( - &mut self.device, + &self.device, &mut self.staging_belt, &mut encoder, view, @@ -200,31 +237,24 @@ impl iced_graphics::window::Compositor for Compositor { // Submit work self.staging_belt.finish(); - self.queue.submit(Some(encoder.finish())); + let _submission = self.queue.submit(Some(encoder.finish())); frame.present(); // Recall staging buffers - self.local_pool - .spawner() - .spawn(self.staging_belt.recall()) - .expect("Recall staging belt"); - - self.local_pool.run_until_stalled(); + self.staging_belt.recall(); Ok(()) } Err(error) => match error { wgpu::SurfaceError::Timeout => { - Err(iced_graphics::window::SurfaceError::Timeout) + Err(compositor::SurfaceError::Timeout) } wgpu::SurfaceError::Outdated => { - Err(iced_graphics::window::SurfaceError::Outdated) - } - wgpu::SurfaceError::Lost => { - Err(iced_graphics::window::SurfaceError::Lost) + Err(compositor::SurfaceError::Outdated) } + wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), wgpu::SurfaceError::OutOfMemory => { - Err(iced_graphics::window::SurfaceError::OutOfMemory) + Err(compositor::SurfaceError::OutOfMemory) } }, } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index f7232248..f66b92fa 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -1,17 +1,18 @@ [package] name = "iced_winit" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A winit runtime for Iced" license = "MIT" -repository = "https://github.com/hecrj/iced" +repository = "https://github.com/iced-rs/iced" documentation = "https://docs.rs/iced_winit" keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] debug = ["iced_native/debug"] +system = ["sysinfo"] [dependencies] window_clipboard = "0.2" @@ -19,20 +20,20 @@ log = "0.4" thiserror = "1.0" [dependencies.winit] -version = "0.26" -git = "https://github.com/iced-rs/winit" -rev = "02a12380960cec2f351c09a33d6a7cc2789d96a6" +version = "0.27" +git = "https://github.com/iced-rs/winit.git" +rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" [dependencies.iced_native] -version = "0.4" +version = "0.5" path = "../native" [dependencies.iced_graphics] -version = "0.2" +version = "0.3" path = "../graphics" [dependencies.iced_futures] -version = "0.3" +version = "0.4" path = "../futures" [target.'cfg(target_os = "windows")'.dependencies.winapi] @@ -41,3 +42,7 @@ version = "0.3.6" [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] version = "0.3" features = ["Document", "Window"] + +[dependencies.sysinfo] +version = "0.23" +optional = true diff --git a/winit/README.md b/winit/README.md index 5a94cd92..3ca46fff 100644 --- a/winit/README.md +++ b/winit/README.md @@ -1,7 +1,7 @@ # `iced_winit` [][documentation] [](https://crates.io/crates/iced_winit) -[](https://github.com/hecrj/iced/blob/master/LICENSE) +[](https://github.com/iced-rs/iced/blob/master/LICENSE) [](https://discord.gg/3xZJ65GAhd) `iced_winit` offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`]. diff --git a/winit/src/application.rs b/winit/src/application.rs index ed077507..0496aea9 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -6,17 +6,22 @@ pub use state::State; use crate::clipboard::{self, Clipboard}; use crate::conversion; use crate::mouse; +use crate::renderer; +use crate::widget::operation; use crate::{ - Color, Command, Debug, Error, Executor, Mode, Proxy, Runtime, Settings, - Size, Subscription, + Command, Debug, Error, Executor, Proxy, Runtime, Settings, Size, + Subscription, }; use iced_futures::futures; use iced_futures::futures::channel::mpsc; +use iced_graphics::compositor; use iced_graphics::window; use iced_native::program::Program; use iced_native::user_interface::{self, UserInterface}; +pub use iced_native::application::{Appearance, StyleSheet}; + use std::mem::ManuallyDrop; /// An interactive, native cross-platform application. @@ -30,7 +35,10 @@ use std::mem::ManuallyDrop; /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Program { +pub trait Application: Program +where + <Self::Renderer as crate::Renderer>::Theme: StyleSheet, +{ /// The data needed to initialize your [`Application`]. type Flags; @@ -50,6 +58,16 @@ pub trait Application: Program { /// title of your application when necessary. fn title(&self) -> String; + /// Returns the current [`Theme`] of the [`Application`]. + fn theme(&self) -> <Self::Renderer as crate::Renderer>::Theme; + + /// Returns the [`Style`] variation of the [`Theme`]. + fn style( + &self, + ) -> <<Self::Renderer as crate::Renderer>::Theme as StyleSheet>::Style { + Default::default() + } + /// Returns the event `Subscription` for the current state of the /// application. /// @@ -63,23 +81,6 @@ pub trait Application: Program { Subscription::none() } - /// Returns the current [`Application`] mode. - /// - /// The runtime will automatically transition your application if a new mode - /// is returned. - /// - /// By default, an application will run in windowed mode. - fn mode(&self) -> Mode { - Mode::Windowed - } - - /// Returns the background [`Color`] of the [`Application`]. - /// - /// By default, it returns [`Color::WHITE`]. - fn background_color(&self) -> Color { - Color::WHITE - } - /// Returns the scale factor of the [`Application`]. /// /// It can be used to dynamically control the size of the UI at runtime @@ -111,18 +112,19 @@ where A: Application + 'static, E: Executor + 'static, C: window::Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as crate::Renderer>::Theme: StyleSheet, { use futures::task; use futures::Future; - use winit::event_loop::EventLoop; + use winit::event_loop::EventLoopBuilder; let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoop::with_user_event(); - let mut proxy = event_loop.create_proxy(); + let event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); - let mut runtime = { + let runtime = { let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; @@ -135,16 +137,15 @@ where runtime.enter(|| A::new(flags)) }; - let subscription = application.subscription(); + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); - let window = settings - .window - .into_builder( - &application.title(), - application.mode(), - event_loop.primary_monitor(), - settings.id, - ) + log::info!("Window builder: {:#?}", builder); + + let window = builder .build(&event_loop) .map_err(Error::WindowCreationFailed)?; @@ -163,17 +164,6 @@ where .expect("Append canvas to HTML body"); } - let mut clipboard = Clipboard::connect(&window); - - run_command( - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &window, - ); - runtime.track(subscription); - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; let (mut sender, receiver) = mpsc::unbounded(); @@ -183,10 +173,10 @@ where compositor, renderer, runtime, - clipboard, proxy, debug, receiver, + init_command, window, settings.exit_on_close_request, )); @@ -196,7 +186,7 @@ where platform::run(event_loop, move |event, _, control_flow| { use winit::event_loop::ControlFlow; - if let ControlFlow::Exit = control_flow { + if let ControlFlow::ExitWithCode(_) = control_flow { return; } @@ -233,20 +223,23 @@ async fn run_instance<A, E, C>( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut clipboard: Clipboard, mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver<winit::event::Event<'_, A::Message>>, + init_command: Command<A::Message>, window: winit::window::Window, exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, C: window::Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as crate::Renderer>::Theme: StyleSheet, { use iced_futures::futures::stream::StreamExt; use winit::event; + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface(&window); let mut state = State::new(&application, &window); @@ -260,9 +253,24 @@ async fn run_instance<A, E, C>( physical_size.height, ); + run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &window, + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + let mut user_interface = ManuallyDrop::new(build_user_interface( - &mut application, - user_interface::Cache::default(), + &application, + cache, &mut renderer, state.logical_size(), &mut debug, @@ -303,18 +311,22 @@ async fn run_instance<A, E, C>( user_interface::State::Outdated, ) { - let cache = + let mut cache = ManuallyDrop::into_inner(user_interface).into_cache(); // Update application update( &mut application, + &mut cache, + &state, + &mut renderer, &mut runtime, &mut clipboard, &mut proxy, &mut debug, &mut messages, &window, + || compositor.fetch_information(), ); // Update window @@ -323,7 +335,7 @@ async fn run_instance<A, E, C>( let should_exit = application.should_exit(); user_interface = ManuallyDrop::new(build_user_interface( - &mut application, + &application, cache, &mut renderer, state.logical_size(), @@ -336,8 +348,14 @@ async fn run_instance<A, E, C>( } debug.draw_started(); - let new_mouse_interaction = - user_interface.draw(&mut renderer, state.cursor_position()); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -354,6 +372,7 @@ async fn run_instance<A, E, C>( event::MacOS::ReceivedUrl(url), )) => { use iced_native::event; + events.push(iced_native::Event::PlatformSpecific( event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( url, @@ -384,8 +403,14 @@ async fn run_instance<A, E, C>( debug.layout_finished(); debug.draw_started(); - let new_mouse_interaction = user_interface - .draw(&mut renderer, state.cursor_position()); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( @@ -420,7 +445,7 @@ async fn run_instance<A, E, C>( } Err(error) => match error { // This is an unrecoverable error. - window::SurfaceError::OutOfMemory => { + compositor::SurfaceError::OutOfMemory => { panic!("{:?}", error); } _ => { @@ -487,12 +512,15 @@ pub fn requests_exit( /// Builds a [`UserInterface`] for the provided [`Application`], logging /// [`struct@Debug`] information accordingly. pub fn build_user_interface<'a, A: Application>( - application: &'a mut A, + application: &'a A, cache: user_interface::Cache, renderer: &mut A::Renderer, size: Size, debug: &mut Debug, -) -> UserInterface<'a, A::Message, A::Renderer> { +) -> UserInterface<'a, A::Message, A::Renderer> +where + <A::Renderer as crate::Renderer>::Theme: StyleSheet, +{ debug.view_started(); let view = application.view(); debug.view_finished(); @@ -508,13 +536,19 @@ pub fn build_user_interface<'a, A: Application>( /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update<A: Application, E: Executor>( application: &mut A, + cache: &mut user_interface::Cache, + state: &State<A>, + renderer: &mut A::Renderer, runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, messages: &mut Vec<A::Message>, window: &winit::window::Window, -) { + graphics_info: impl FnOnce() -> compositor::Information + Copy, +) where + <A::Renderer as crate::Renderer>::Theme: StyleSheet, +{ for message in messages.drain(..) { debug.log_message(&message); @@ -522,7 +556,19 @@ pub fn update<A: Application, E: Executor>( let command = runtime.enter(|| application.update(message)); debug.update_finished(); - run_command(command, runtime, clipboard, proxy, window); + run_command( + application, + cache, + state, + renderer, + command, + runtime, + clipboard, + proxy, + debug, + window, + graphics_info, + ); } let subscription = application.subscription(); @@ -530,14 +576,25 @@ pub fn update<A: Application, E: Executor>( } /// Runs the actions of a [`Command`]. -pub fn run_command<Message: 'static + std::fmt::Debug + Send, E: Executor>( - command: Command<Message>, - runtime: &mut Runtime<E, Proxy<Message>, Message>, +pub fn run_command<A, E>( + application: &A, + cache: &mut user_interface::Cache, + state: &State<A>, + renderer: &mut A::Renderer, + command: Command<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy<Message>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, window: &winit::window::Window, -) { + _graphics_info: impl FnOnce() -> compositor::Information + Copy, +) where + A: Application, + E: Executor, + <A::Renderer as crate::Renderer>::Theme: StyleSheet, +{ use iced_native::command; + use iced_native::system; use iced_native::window; for action in command.actions() { @@ -570,7 +627,76 @@ pub fn run_command<Message: 'static + std::fmt::Debug + Send, E: Executor>( y, }); } + window::Action::SetMode(mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(tag(mode)) + .expect("Send message to event loop"); + } + }, + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + let graphics_info = _graphics_info(); + 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("Send message to event loop") + }); + } + } }, + command::Action::Widget(action) => { + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + ); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } } } } diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index b54d3aed..9c539548 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -1,31 +1,38 @@ +use crate::application::{self, StyleSheet as _}; use crate::conversion; -use crate::{Application, Color, Debug, Mode, Point, Size, Viewport}; +use crate::{Application, Color, Debug, Point, Size, Viewport}; use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; use winit::window::Window; /// The state of a windowed [`Application`]. -#[derive(Debug, Clone)] -pub struct State<A: Application> { +#[allow(missing_debug_implementations)] +pub struct State<A: Application> +where + <A::Renderer as crate::Renderer>::Theme: application::StyleSheet, +{ title: String, - mode: Mode, - background_color: Color, scale_factor: f64, viewport: Viewport, viewport_version: usize, cursor_position: winit::dpi::PhysicalPosition<f64>, modifiers: winit::event::ModifiersState, + theme: <A::Renderer as crate::Renderer>::Theme, + appearance: application::Appearance, application: PhantomData<A>, } -impl<A: Application> State<A> { +impl<A: Application> State<A> +where + <A::Renderer as crate::Renderer>::Theme: application::StyleSheet, +{ /// Creates a new [`State`] for the provided [`Application`] and window. pub fn new(application: &A, window: &Window) -> Self { let title = application.title(); - let mode = application.mode(); - let background_color = application.background_color(); let scale_factor = application.scale_factor(); + let theme = application.theme(); + let appearance = theme.appearance(application.style()); let viewport = { let physical_size = window.inner_size(); @@ -38,23 +45,18 @@ impl<A: Application> State<A> { Self { title, - mode, - background_color, scale_factor, viewport, viewport_version: 0, // TODO: Encode cursor availability in the type-system cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), modifiers: winit::event::ModifiersState::default(), + theme, + appearance, application: PhantomData, } } - /// Returns the current background [`Color`] of the [`State`]. - pub fn background_color(&self) -> Color { - self.background_color - } - /// Returns the current [`Viewport`] of the [`State`]. pub fn viewport(&self) -> &Viewport { &self.viewport @@ -95,6 +97,21 @@ impl<A: Application> State<A> { self.modifiers } + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &<A::Renderer as crate::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( @@ -173,35 +190,26 @@ impl<A: Application> State<A> { self.title = new_title; } - // Update window mode - let new_mode = application.mode(); - - if self.mode != new_mode { - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - new_mode, - )); - - window.set_visible(conversion::visible(new_mode)); - - self.mode = new_mode; - } - - // Update background color - self.background_color = application.background_color(); - - // Update scale factor + // Update scale factor and size let new_scale_factor = application.scale_factor(); + let new_size = window.inner_size(); + let current_size = self.viewport.physical_size(); - if self.scale_factor != new_scale_factor { - let size = window.inner_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(size.width, size.height), + 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(); + self.appearance = self.theme.appearance(application.style()); } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index b00a095d..ba5b0002 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -1,12 +1,12 @@ //! Convert [`winit`] types into [`iced_native`] types, and viceversa. //! //! [`winit`]: https://github.com/rust-windowing/winit -//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native use crate::keyboard; use crate::mouse; use crate::touch; use crate::window; -use crate::{Event, Mode, Point, Position}; +use crate::{Event, Point, Position}; /// Converts a winit window event into an iced event. pub fn window_event( @@ -182,33 +182,43 @@ pub fn position( } } -/// Converts a [`Mode`] to a [`winit`] fullscreen mode. +/// Converts a [`window::Mode`] to a [`winit`] fullscreen mode. /// /// [`winit`]: https://github.com/rust-windowing/winit pub fn fullscreen( monitor: Option<winit::monitor::MonitorHandle>, - mode: Mode, + mode: window::Mode, ) -> Option<winit::window::Fullscreen> { match mode { - Mode::Windowed | Mode::Hidden => None, - Mode::Fullscreen => { + window::Mode::Windowed | window::Mode::Hidden => None, + window::Mode::Fullscreen => { Some(winit::window::Fullscreen::Borderless(monitor)) } } } -/// Converts a [`Mode`] to a visibility flag. -pub fn visible(mode: Mode) -> bool { +/// Converts a [`window::Mode`] to a visibility flag. +pub fn visible(mode: window::Mode) -> bool { match mode { - Mode::Windowed | Mode::Fullscreen => true, - Mode::Hidden => false, + window::Mode::Windowed | window::Mode::Fullscreen => true, + window::Mode::Hidden => false, + } +} + +/// Converts a [`winit`] fullscreen mode to a [`window::Mode`]. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode { + match mode { + None => window::Mode::Windowed, + Some(_) => window::Mode::Fullscreen, } } /// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native pub fn mouse_interaction( interaction: mouse::Interaction, ) -> winit::window::CursorIcon { @@ -232,7 +242,7 @@ pub fn mouse_interaction( /// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { match mouse_button { winit::event::MouseButton::Left => mouse::Button::Left, @@ -248,7 +258,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// modifiers state. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native pub fn modifiers( modifiers: winit::event::ModifiersState, ) -> keyboard::Modifiers { @@ -275,7 +285,7 @@ pub fn cursor_position( /// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native pub fn touch_event( touch: winit::event::Touch, scale_factor: f64, @@ -306,7 +316,7 @@ pub fn touch_event( /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native pub fn key_code( virtual_keycode: winit::event::VirtualKeyCode, ) -> keyboard::KeyCode { @@ -485,10 +495,10 @@ pub fn key_code( // As defined in: http://www.unicode.org/faq/private_use.html pub(crate) fn is_private_use_character(c: char) -> bool { - match c { + matches!( + c, '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' - | '\u{100000}'..='\u{10FFFD}' => true, - _ => false, - } + | '\u{100000}'..='\u{10FFFD}' + ) } diff --git a/winit/src/error.rs b/winit/src/error.rs index 8e1d20e8..eaeafd51 100644 --- a/winit/src/error.rs +++ b/winit/src/error.rs @@ -11,17 +11,13 @@ pub enum Error { #[error("the application window could not be created")] WindowCreationFailed(winit::error::OsError), - /// A suitable graphics adapter or device could not be found. - #[error("a suitable graphics adapter or device could not be found")] - GraphicsAdapterNotFound, + /// The application graphics context could not be created. + #[error("the application graphics context could not be created")] + GraphicsCreationFailed(iced_graphics::Error), } impl From<iced_graphics::Error> for Error { fn from(error: iced_graphics::Error) -> Error { - match error { - iced_graphics::Error::AdapterNotFound => { - Error::GraphicsAdapterNotFound - } - } + Error::GraphicsCreationFailed(error) } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index b31adf6e..e32cc9af 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1,6 +1,6 @@ //! A windowing shell for Iced, on top of [`winit`]. //! -//!  +//!  //! //! `iced_winit` offers some convenient abstractions on top of [`iced_native`] //! to quickstart development when using [`winit`]. @@ -11,17 +11,25 @@ //! Additionally, a [`conversion`] module is available for users that decide to //! implement a custom event loop. //! -//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.4/native //! [`winit`]: https://github.com/rust-windowing/winit //! [`conversion`]: crate::conversion #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![deny(missing_docs)] -#![deny(missing_debug_implementations)] -#![deny(unused_results)] -#![forbid(unsafe_code)] -#![forbid(rust_2018_idioms)] +#![deny( + missing_debug_implementations, + missing_docs, + unused_results, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] +#![forbid(rust_2018_idioms, unsafe_code)] +#![allow(clippy::inherent_to_string, clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_cfg))] #[doc(no_inline)] pub use iced_native::*; @@ -33,15 +41,16 @@ pub mod conversion; pub mod settings; pub mod window; +#[cfg(feature = "system")] +pub mod system; + mod error; -mod mode; mod position; mod proxy; pub use application::Application; pub use clipboard::Clipboard; pub use error::Error; -pub use mode::Mode; pub use position::Position; pub use proxy::Proxy; pub use settings::Settings; diff --git a/winit/src/mode.rs b/winit/src/mode.rs deleted file mode 100644 index fdce8e23..00000000 --- a/winit/src/mode.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// The mode of a window-based application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Mode { - /// The application appears in its own window. - Windowed, - - /// The application takes the whole screen of its current monitor. - Fullscreen, - - /// The application is hidden - Hidden, -} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 9a93824a..6387454b 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -14,7 +14,7 @@ mod platform; pub use platform::PlatformSpecific; use crate::conversion; -use crate::{Mode, Position}; +use crate::Position; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; @@ -37,12 +37,16 @@ pub struct Settings<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, /// Whether the [`Application`] should try to build the context /// using OpenGL ES first then OpenGL. /// /// NOTE: Only works for the `glow` backend. + /// + /// [`Application`]: crate::Application pub try_opengles_first: bool, } @@ -61,6 +65,9 @@ pub struct Window { /// 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, @@ -85,7 +92,6 @@ impl Window { pub fn into_builder( self, title: &str, - mode: Mode, primary_monitor: Option<MonitorHandle>, _id: Option<String>, ) -> WindowBuilder { @@ -101,7 +107,7 @@ impl Window { .with_transparent(self.transparent) .with_window_icon(self.icon) .with_always_on_top(self.always_on_top) - .with_visible(conversion::visible(mode)); + .with_visible(self.visible); if let Some(position) = conversion::position( primary_monitor.as_ref(), @@ -132,7 +138,7 @@ impl Window { use ::winit::platform::unix::WindowBuilderExtUnix; if let Some(id) = _id { - window_builder = window_builder.with_app_id(id); + window_builder = window_builder.with_name(id.clone(), id); } } @@ -162,9 +168,6 @@ impl Window { ); } - window_builder = window_builder - .with_fullscreen(conversion::fullscreen(primary_monitor, mode)); - window_builder } } @@ -176,6 +179,7 @@ impl Default for Window { position: Position::default(), min_size: None, max_size: None, + visible: true, resizable: true, decorations: true, transparent: false, diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs index fc26acd7..9bef1eaf 100644 --- a/winit/src/settings/windows.rs +++ b/winit/src/settings/windows.rs @@ -5,7 +5,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { /// Parent window - pub parent: Option<winapi::shared::windef::HWND>, + pub parent: Option<winit::platform::windows::HWND>, /// Drag and drop support pub drag_and_drop: bool, diff --git a/winit/src/system.rs b/winit/src/system.rs new file mode 100644 index 00000000..0303707e --- /dev/null +++ b/winit/src/system.rs @@ -0,0 +1,41 @@ +//! Access the native system. +use crate::command::{self, Command}; +pub use iced_native::system::*; + +use iced_graphics::compositor; + +/// Query for available system information. +pub fn fetch_information<Message>( + f: impl Fn(Information) -> Message + Send + 'static, +) -> Command<Message> { + Command::single(command::Action::System(Action::QueryInformation( + Box::new(f), + ))) +} + +pub(crate) fn information( + graphics_info: compositor::Information, +) -> Information { + use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; + let mut system = System::new_all(); + system.refresh_all(); + + let cpu = system.global_processor_info(); + + let memory_used = sysinfo::get_current_pid() + .and_then(|pid| system.process(pid).ok_or("Process not found")) + .map(|process| process.memory()) + .ok(); + + Information { + system_name: system.name(), + system_kernel: system.kernel_version(), + system_version: system.long_os_version(), + cpu_brand: cpu.brand().into(), + cpu_cores: system.physical_core_count(), + memory_total: system.total_memory(), + memory_used, + graphics_adapter: graphics_info.adapter, + graphics_backend: graphics_info.backend, + } +} diff --git a/winit/src/window.rs b/winit/src/window.rs index f3207e68..265139f7 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -2,7 +2,7 @@ use crate::command::{self, Command}; use iced_native::window; -pub use window::Event; +pub use window::{Event, Mode}; /// Resizes the window to the given logical dimensions. pub fn resize<Message>(width: u32, height: u32) -> Command<Message> { @@ -16,3 +16,17 @@ pub fn resize<Message>(width: u32, height: u32) -> Command<Message> { pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> { Command::single(command::Action::Window(window::Action::Move { x, y })) } + +/// Sets the [`Mode`] of the window. +pub fn set_mode<Message>(mode: Mode) -> Command<Message> { + Command::single(command::Action::Window(window::Action::SetMode(mode))) +} + +/// Fetches the current [`Mode`] of the window. +pub fn fetch_mode<Message>( + f: impl FnOnce(Mode) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(window::Action::FetchMode( + Box::new(f), + ))) +} |