diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 09b3169791..20ef2b7352 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -28,7 +28,7 @@ body: If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly! - [the `wgpu` examples]: https://github.com/gfx-rs/wgpu/tree/master/wgpu/examples + [the `wgpu` examples]: https://github.com/gfx-rs/wgpu/tree/trunk/examples [the `glow` examples]: https://github.com/grovesNL/glow/tree/main/examples options: - label: My hardware is compatible and my graphics drivers are up-to-date. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5716979605..40e9235a63 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -8,7 +8,7 @@ jobs: vulnerabilities: runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - name: Install cargo-audit run: cargo install cargo-audit - uses: actions/checkout@master @@ -17,14 +17,14 @@ jobs: - name: Audit vulnerabilities run: cargo audit - artifacts: - runs-on: ubuntu-latest - steps: - - uses: hecrj/setup-rust-action@v1 - - name: Install cargo-outdated - run: cargo install cargo-outdated - - uses: actions/checkout@master - - name: Delete `web-sys` dependency from `integration` example - run: sed -i '$d' examples/integration/Cargo.toml - - name: Find outdated dependencies - run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle + # artifacts: + # runs-on: ubuntu-latest + # steps: + # - uses: hecrj/setup-rust-action@v2 + # - name: Install cargo-outdated + # run: cargo install cargo-outdated + # - uses: actions/checkout@master + # - name: Delete `web-sys` dependency from `integration` example + # run: sed -i '$d' examples/integration/Cargo.toml + # - name: Find outdated dependencies + # run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7cfbff8957..ba1ab00368 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: todos_linux: runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - name: Install cargo-deb run: cargo install cargo-deb - uses: actions/checkout@master @@ -36,7 +36,7 @@ jobs: todos_windows: runs-on: windows-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@master - name: Enable static CRT linkage run: | @@ -56,7 +56,7 @@ jobs: todos_macos: runs-on: macOS-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@master - name: Build todos binary env: @@ -73,7 +73,7 @@ jobs: todos_raspberry: runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@master - name: Install cross run: cargo install cross diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index df9c480fbb..4107e6188f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,7 +4,7 @@ jobs: widget: runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@master - name: Check standalone `iced_widget` crate run: cargo check --package iced_widget --features image,svg,canvas @@ -14,7 +14,7 @@ jobs: env: RUSTFLAGS: --cfg=web_sys_unstable_apis steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: rust-version: stable targets: wasm32-unknown-unknown diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 35bf10f428..ba482215a5 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -6,7 +6,7 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: rust-version: nightly-2023-12-11 - uses: actions/checkout@v2 @@ -16,7 +16,6 @@ jobs: cargo doc --no-deps --all-features \ -p iced_core \ -p iced_highlighter \ - -p iced_style \ -p iced_futures \ -p iced_runtime \ -p iced_graphics \ diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 42a9641105..3aa7eaf3a1 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -4,7 +4,7 @@ jobs: all: runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: components: rustfmt - uses: actions/checkout@master diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2ff86614ad..ccf79cb7e5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ jobs: all: runs-on: macOS-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: components: clippy - uses: actions/checkout@master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c5ee0d949..47c61f5e85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] rust: [stable, beta] steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} - uses: actions/checkout@master diff --git a/CHANGELOG.md b/CHANGELOG.md index 748bdfd01b..16a69a7aa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280) + +Many thanks to... + +- @n1ght-hunter + +## [0.12.1] - 2024-02-22 +### Added +- `extend` and `from_vec` methods for `Column` and `Row`. [#2264](https://github.com/iced-rs/iced/pull/2264) +- `PartialOrd`, `Ord`, and `Hash` implementations for `keyboard::Modifiers`. [#2270](https://github.com/iced-rs/iced/pull/2270) +- `clipboard` module in `advanced` module. [#2272](https://github.com/iced-rs/iced/pull/2272) +- Default `disabled` style for `checkbox` and `hovered` style for `Svg`. [#2273](https://github.com/iced-rs/iced/pull/2273) +- `From` and `From` implementations for `border::Radius`. [#2274](https://github.com/iced-rs/iced/pull/2274) +- `size_hint` method for `Component` trait. [#2275](https://github.com/iced-rs/iced/pull/2275) + +### Fixed +- Black images when using OpenGL backend in `iced_wgpu`. [#2259](https://github.com/iced-rs/iced/pull/2259) +- Documentation for `horizontal_space` and `vertical_space` helpers. [#2265](https://github.com/iced-rs/iced/pull/2265) +- WebAssembly platform. [#2271](https://github.com/iced-rs/iced/pull/2271) +- Decouple `Key` from `keyboard::Modifiers` and apply them to `text` in `KeyboardInput`. [#2238](https://github.com/iced-rs/iced/pull/2238) +- Text insertion not being prioritized in `TextInput` and `TextEditor`. [#2278](https://github.com/iced-rs/iced/pull/2278) +- `iced_tiny_skia` clipping line strokes. [#2282](https://github.com/iced-rs/iced/pull/2282) + +Many thanks to... + +- @PolyMeilex +- @rizzen-yazston +- @wash2 + +## [0.12.0] - 2024-02-15 +### Added - Multi-window support. [#1964](https://github.com/iced-rs/iced/pull/1964) - `TextEditor` widget (or multi-line text input). [#2123](https://github.com/iced-rs/iced/pull/2123) - `Shader` widget. [#2085](https://github.com/iced-rs/iced/pull/2085) @@ -17,10 +48,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Themer` widget. [#2209](https://github.com/iced-rs/iced/pull/2209) - `Transform` primitive. [#2120](https://github.com/iced-rs/iced/pull/2120) - Cut functionality for `TextEditor`. [#2215](https://github.com/iced-rs/iced/pull/2215) -- Disabled support for `Checkbox`. [#2109](https://github.com/iced-rs/iced/pull/2109) +- Primary clipboard support. [#2240](https://github.com/iced-rs/iced/pull/2240) +- Disabled state for `Checkbox`. [#2109](https://github.com/iced-rs/iced/pull/2109) - `skip_taskbar` window setting for Windows. [#2211](https://github.com/iced-rs/iced/pull/2211) - `fetch_maximized` and `fetch_minimized` commands in `window`. [#2189](https://github.com/iced-rs/iced/pull/2189) - `run_with_handle` command in `window`. [#2200](https://github.com/iced-rs/iced/pull/2200) +- `show_system_menu` command in `window`. [#2243](https://github.com/iced-rs/iced/pull/2243) - `text_shaping` method for `Tooltip`. [#2172](https://github.com/iced-rs/iced/pull/2172) - `interaction` method for `MouseArea`. [#2207](https://github.com/iced-rs/iced/pull/2207) - `hovered` styling for `Svg` widget. [#2163](https://github.com/iced-rs/iced/pull/2163) @@ -40,10 +73,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mouse movement events for `MouseArea`. [#2147](https://github.com/iced-rs/iced/pull/2147) - Dracula, Nord, Solarized, and Gruvbox variants for `Theme`. [#2170](https://github.com/iced-rs/iced/pull/2170) - Catppuccin, Tokyo Night, Kanagawa, Moonfly, Nightfly and Oxocarbon variants for `Theme`. [#2233](https://github.com/iced-rs/iced/pull/2233) - - `From where T: Into` for `svg::Handle`. [#2235](https://github.com/iced-rs/iced/pull/2235) - `on_open` and `on_close` handlers for `PickList`. [#2174](https://github.com/iced-rs/iced/pull/2174) - Support for generic `Element` in `Tooltip`. [#2228](https://github.com/iced-rs/iced/pull/2228) +- Container and `gap` styling for `Scrollable`. [#2239](https://github.com/iced-rs/iced/pull/2239) +- Use `Borrow` for both `options` and `selected` in PickList. [#2251](https://github.com/iced-rs/iced/pull/2251) +- `clip` property for `Container`, `Column`, `Row`, and `Button`. #[2252](https://github.com/iced-rs/iced/pull/2252) ### Changed - Enable WebGPU backend in `wgpu` by default instead of WebGL. [#2068](https://github.com/iced-rs/iced/pull/2068) @@ -67,6 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add a capacity limit to the `GlyphCache` in `iced_tiny_skia`. [#2210](https://github.com/iced-rs/iced/pull/2210) - Use pointer equality to speed up `PartialEq` implementation of `image::Bytes`. [#2220](https://github.com/iced-rs/iced/pull/2220) - Update `bitflags`, `glam`, `kurbo`, `ouroboros`, `qrcode`, and `sysinfo` dependencies. [#2227](https://github.com/iced-rs/iced/pull/2227) +- Improve some widget ergonomics. [#2253](https://github.com/iced-rs/iced/pull/2253) ### Fixed - Clipping of `TextInput` selection. [#2199](https://github.com/iced-rs/iced/pull/2199) @@ -75,6 +111,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PaneGrid` click interaction on the top edge. [#2168](https://github.com/iced-rs/iced/pull/2168) - `iced_wgpu` not rendering text in SVGs. [#2161](https://github.com/iced-rs/iced/pull/2161) - Text clipping. [#2154](https://github.com/iced-rs/iced/pull/2154) +- Text transparency in `iced_tiny_skia`. [#2250](https://github.com/iced-rs/iced/pull/2250) - Layout invalidation when `Tooltip` changes `overlay`. [#2143](https://github.com/iced-rs/iced/pull/2143) - `Overlay` composition. [#2142](https://github.com/iced-rs/iced/pull/2142) - Incorrect GIF for the `progress_bar` example. [#2141](https://github.com/iced-rs/iced/pull/2141) @@ -108,6 +145,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Alpha mode misconfiguration in `iced_wgpu`. [#2231](https://github.com/iced-rs/iced/pull/2231) - Outdated documentation leading to a dead link. [#2232](https://github.com/iced-rs/iced/pull/2232) + Many thanks to... - @akshayr-mecha @@ -150,6 +188,7 @@ Many thanks to... - @nyurik - @Remmirad - @ripytide +- @snaggen - @Tahinli - @tarkah - @tzemanovic @@ -733,7 +772,9 @@ Many thanks to... ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.10.0...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.12.1...HEAD +[0.12.1]: https://github.com/iced-rs/iced/compare/0.12.0...0.12.1 +[0.12.0]: https://github.com/iced-rs/iced/compare/0.10.0...0.12.0 [0.10.0]: https://github.com/iced-rs/iced/compare/0.9.0...0.10.0 [0.9.0]: https://github.com/iced-rs/iced/compare/0.8.0...0.9.0 [0.8.0]: https://github.com/iced-rs/iced/compare/0.7.0...0.8.0 diff --git a/Cargo.toml b/Cargo.toml index de52cd8048..f446e2af57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu"] +default = ["wgpu", "fira-sans"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget @@ -39,8 +39,6 @@ tokio = ["iced_futures/tokio"] async-std = ["iced_futures/async-std"] # Enables `smol` as the `executor::Default` on native platforms smol = ["iced_futures/smol"] -# Enables advanced color conversion via `palette` -palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] # Enables broken "sRGB linear" blending to reproduce color management of the Web @@ -53,9 +51,10 @@ highlighter = ["iced_highlighter"] multi-window = ["iced_winit/multi-window"] # Enables the advanced module advanced = [] +# Enables embedding Fira Sans as the default font on Wasm builds +fira-sans = ["iced_renderer/fira-sans"] [dependencies] -iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true @@ -88,7 +87,6 @@ members = [ "highlighter", "renderer", "runtime", - "style", "tiny_skia", "wgpu", "widget", @@ -97,7 +95,7 @@ members = [ ] [workspace.package] -version = "0.12.0" +version = "0.13.0-dev" authors = ["Héctor Ramón Jiménez "] edition = "2021" license = "MIT" @@ -107,18 +105,17 @@ categories = ["gui"] keywords = ["gui", "ui", "graphics", "interface", "widgets"] [workspace.dependencies] -iced = { version = "0.12", path = "." } -iced_core = { version = "0.12", path = "core" } -iced_futures = { version = "0.12", path = "futures" } -iced_graphics = { version = "0.12", path = "graphics" } -iced_highlighter = { version = "0.12", path = "highlighter" } -iced_renderer = { version = "0.12", path = "renderer" } -iced_runtime = { version = "0.12", path = "runtime" } -iced_style = { version = "0.12", path = "style" } -iced_tiny_skia = { version = "0.12", path = "tiny_skia" } -iced_wgpu = { version = "0.12", path = "wgpu" } -iced_widget = { version = "0.12", path = "widget" } -iced_winit = { version = "0.12", path = "winit" } +iced = { version = "0.13.0-dev", path = "." } +iced_core = { version = "0.13.0-dev", path = "core" } +iced_futures = { version = "0.13.0-dev", path = "futures" } +iced_graphics = { version = "0.13.0-dev", path = "graphics" } +iced_highlighter = { version = "0.13.0-dev", path = "highlighter" } +iced_renderer = { version = "0.13.0-dev", path = "renderer" } +iced_runtime = { version = "0.13.0-dev", path = "runtime" } +iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" } +iced_wgpu = { version = "0.13.0-dev", path = "wgpu" } +iced_widget = { version = "0.13.0-dev", path = "widget" } +iced_winit = { version = "0.13.0-dev", path = "winit" } async-std = "1.0" bitflags = "2.0" @@ -160,5 +157,5 @@ web-sys = "=0.3.67" web-time = "0.2" wgpu = "0.19" winapi = "0.3" -window_clipboard = "0.4" +window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } diff --git a/README.md b/README.md index eb2befbc4c..0db09ded0c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Inspired by [Elm]. * Simple, easy-to-use, batteries-included API * Type-safe, reactive programming model -* [Cross-platform support] (Windows, macOS, Linux, and [the Web]) +* [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!) @@ -46,7 +46,6 @@ __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://github.com/iced-rs/iced_web [text inputs]: https://iced.rs/examples/text_input.mp4 [scrollables]: https://iced.rs/examples/scrollable.mp4 [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4 @@ -69,7 +68,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.10" +iced = "0.12" ``` If your project is using a Rust edition older than 2021, then you will need to @@ -99,8 +98,8 @@ that can be incremented and decremented using two buttons. We start by modelling the __state__ of our application: ```rust +#[derive(Default)] struct Counter { - // The counter value value: i32, } ``` @@ -111,8 +110,8 @@ the button presses. These interactions are our __messages__: ```rust #[derive(Debug, Clone, Copy)] pub enum Message { - IncrementPressed, - DecrementPressed, + Increment, + Decrement, } ``` @@ -127,15 +126,15 @@ impl Counter { // We use a column: a simple vertical layout column![ // The increment button. We tell it to produce an - // `IncrementPressed` message when pressed - button("+").on_press(Message::IncrementPressed), + // `Increment` message when pressed + button("+").on_press(Message::Increment), // 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), + // `Decrement` message when pressed + button("-").on_press(Message::Decrement), ] } } @@ -150,10 +149,10 @@ impl Counter { pub fn update(&mut self, message: Message) { match message { - Message::IncrementPressed => { + Message::Increment => { self.value += 1; } - Message::DecrementPressed => { + Message::Decrement => { self.value -= 1; } } @@ -161,15 +160,22 @@ impl Counter { } ``` -And that's everything! We just wrote a whole user interface. Iced is now able -to: +And that's everything! We just wrote a whole user interface. Let's run it: + +```rust +fn main() -> iced::Result { + iced::run("A cool counter", Counter::update, Counter::view) +} +``` + +Iced will automatically: 1. Take the result of our __view logic__ and layout its widgets. 1. Process events from our system and produce __messages__ for our __update logic__. 1. Draw the resulting user interface. -Browse the [documentation] and the [examples] to learn more! +Read the [book], the [documentation], and the [examples] to learn more! ## Implementation details @@ -209,6 +215,7 @@ come chat to [our Discord server]. The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com] +[book]: https://book.iced.rs/ [documentation]: https://docs.rs/iced/ [examples]: https://github.com/iced-rs/iced/tree/master/examples [Coffee]: https://github.com/hecrj/coffee diff --git a/core/Cargo.toml b/core/Cargo.toml index 2360e82291..c273fcb4d9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,14 +15,13 @@ bitflags.workspace = true glam.workspace = true log.workspace = true num-traits.workspace = true +once_cell.workspace = true +palette.workspace = true smol_str.workspace = true thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true -palette.workspace = true -palette.optional = true - [target.'cfg(windows)'.dependencies] raw-window-handle.workspace = true diff --git a/core/src/angle.rs b/core/src/angle.rs index 30ddad834d..dc3c0e93df 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -7,6 +7,18 @@ use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Degrees(pub f32); +impl PartialEq for Degrees { + fn eq(&self, other: &f32) -> bool { + self.0.eq(other) + } +} + +impl PartialOrd for Degrees { + fn partial_cmp(&self, other: &f32) -> Option { + self.0.partial_cmp(other) + } +} + /// Radians #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Radians(pub f32); @@ -140,3 +152,15 @@ impl Div for Radians { Self(self.0 / rhs.0) } } + +impl PartialEq for Radians { + fn eq(&self, other: &f32) -> bool { + self.0.eq(other) + } +} + +impl PartialOrd for Radians { + fn partial_cmp(&self, other: &f32) -> Option { + self.0.partial_cmp(other) + } +} diff --git a/core/src/background.rs b/core/src/background.rs index 347c52c0f7..c8b7cbeaca 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -11,6 +11,19 @@ pub enum Background { // TODO: Add image variant } +impl Background { + /// Scales the alpha channel of the [`Background`] by the given + /// factor. + pub fn scale_alpha(self, factor: f32) -> Self { + match self { + Self::Color(color) => Self::Color(color.scale_alpha(factor)), + Self::Gradient(gradient) => { + Self::Gradient(gradient.scale_alpha(factor)) + } + } + } +} + impl From for Background { fn from(color: Color) -> Self { Background::Color(color) diff --git a/core/src/border.rs b/core/src/border.rs index 2182334129..2df24988b7 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -1,5 +1,5 @@ //! Draw lines around containers. -use crate::Color; +use crate::{Color, Pixels}; /// A border. #[derive(Debug, Clone, Copy, PartialEq, Default)] @@ -15,11 +15,38 @@ pub struct Border { } impl Border { - /// Creates a new default [`Border`] with the given [`Radius`]. - pub fn with_radius(radius: impl Into) -> Self { + /// Creates a new default rounded [`Border`] with the given [`Radius`]. + /// + /// ``` + /// # use iced_core::Border; + /// # + /// assert_eq!(Border::rounded(10), Border::default().with_radius(10)); + /// ``` + pub fn rounded(radius: impl Into) -> Self { + Self::default().with_radius(radius) + } + + /// Updates the [`Color`] of the [`Border`]. + pub fn with_color(self, color: impl Into) -> Self { + Self { + color: color.into(), + ..self + } + } + + /// Updates the [`Radius`] of the [`Border`]. + pub fn with_radius(self, radius: impl Into) -> Self { Self { radius: radius.into(), - ..Self::default() + ..self + } + } + + /// Updates the width of the [`Border`]. + pub fn with_width(self, width: impl Into) -> Self { + Self { + width: width.into().0, + ..self } } } @@ -37,7 +64,19 @@ impl From for Radius { impl From for Radius { fn from(w: u8) -> Self { - Self([f32::from(w); 4]) + Self::from(f32::from(w)) + } +} + +impl From for Radius { + fn from(w: u16) -> Self { + Self::from(f32::from(w)) + } +} + +impl From for Radius { + fn from(w: i32) -> Self { + Self::from(w as f32) } } diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index 081b40046c..5df3e26758 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -4,10 +4,21 @@ /// applications. pub trait Clipboard { /// Reads the current content of the [`Clipboard`] as text. - fn read(&self) -> Option; + fn read(&self, kind: Kind) -> Option; /// Writes the given text contents to the [`Clipboard`]. - fn write(&mut self, contents: String); + fn write(&mut self, kind: Kind, contents: String); +} + +/// The kind of [`Clipboard`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// The standard clipboard. + Standard, + /// The primary clipboard. + /// + /// Normally only present in X11 and Wayland. + Primary, } /// A null implementation of the [`Clipboard`] trait. @@ -15,9 +26,9 @@ pub trait Clipboard { pub struct Null; impl Clipboard for Null { - fn read(&self) -> Option { + fn read(&self, _kind: Kind) -> Option { None } - fn write(&mut self, _contents: String) {} + fn write(&mut self, _kind: Kind, _contents: String) {} } diff --git a/core/src/color.rs b/core/src/color.rs index b8db322fae..4e79defb12 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "palette")] use palette::rgb::{Srgb, Srgba}; /// A color in the `sRGB` color space. @@ -151,6 +150,14 @@ impl Color { pub fn inverse(self) -> Color { Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) } + + /// Scales the alpha channel of the [`Color`] by the given factor. + pub fn scale_alpha(self, factor: f32) -> Color { + Self { + a: self.a * factor, + ..self + } + } } impl From<[f32; 3]> for Color { @@ -202,7 +209,6 @@ macro_rules! color { }}; } -#[cfg(feature = "palette")] /// Converts from palette's `Rgba` type to a [`Color`]. impl From for Color { fn from(rgba: Srgba) -> Self { @@ -210,7 +216,6 @@ impl From for Color { } } -#[cfg(feature = "palette")] /// Converts from [`Color`] to palette's `Rgba` type. impl From for Srgba { fn from(c: Color) -> Self { @@ -218,7 +223,6 @@ impl From for Srgba { } } -#[cfg(feature = "palette")] /// Converts from palette's `Rgb` type to a [`Color`]. impl From for Color { fn from(rgb: Srgb) -> Self { @@ -226,7 +230,6 @@ impl From for Color { } } -#[cfg(feature = "palette")] /// Converts from [`Color`] to palette's `Rgb` type. impl From for Srgb { fn from(c: Color) -> Self { @@ -234,7 +237,6 @@ impl From for Srgb { } } -#[cfg(feature = "palette")] #[cfg(test)] mod tests { use super::*; diff --git a/core/src/element.rs b/core/src/element.rs index 8eea90ca98..989eaa3b3d 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -94,52 +94,34 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// producing them. Let's implement our __view logic__ now: /// /// ```no_run - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; + /// # mod iced { + /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>; /// # - /// # impl Counter { - /// # pub fn view( - /// # &self, - /// # ) -> iced_core::Element { + /// # pub mod widget { + /// # pub fn row<'a, Message>(iter: impl IntoIterator>) -> super::Element<'a, Message> { /// # unimplemented!() /// # } /// # } /// # } /// # - /// # mod iced { - /// # pub use iced_core::renderer::Null as Renderer; - /// # pub use iced_core::Element; - /// # - /// # pub mod widget { - /// # pub struct Row { - /// # _t: std::marker::PhantomData, - /// # } - /// # - /// # impl Row { - /// # pub fn new() -> Self { - /// # unimplemented!() - /// # } + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; /// # - /// # pub fn spacing(mut self, _: u32) -> Self { - /// # unimplemented!() - /// # } + /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>; /// # - /// # pub fn push( - /// # mut self, - /// # _: iced_core::Element, - /// # ) -> Self { - /// # unimplemented!() - /// # } + /// # impl Counter { + /// # pub fn view(&self) -> Element { + /// # unimplemented!() /// # } /// # } /// # } /// # /// use counter::Counter; /// - /// use iced::widget::Row; - /// use iced::{Element, Renderer}; + /// use iced::widget::row; + /// use iced::Element; /// /// struct ManyCounters { /// counters: Vec, @@ -151,24 +133,21 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// } /// /// impl ManyCounters { - /// pub fn view(&mut self) -> Row { - /// // We can quickly populate a `Row` by folding over our counters - /// self.counters.iter_mut().enumerate().fold( - /// Row::new().spacing(20), - /// |row, (index, counter)| { - /// // We display the counter - /// let element: Element = - /// counter.view().into(); - /// - /// row.push( + /// pub fn view(&self) -> Element { + /// // We can quickly populate a `row` by mapping our counters + /// row( + /// self.counters + /// .iter() + /// .map(Counter::view) + /// .enumerate() + /// .map(|(index, counter)| { /// // Here we turn our `Element` into /// // an `Element` by combining the `index` and the /// // message of the `element`. - /// element - /// .map(move |message| Message::Counter(index, message)), - /// ) - /// }, + /// counter.map(move |message| Message::Counter(index, message)) + /// }), /// ) + /// .into() /// } /// } /// ``` diff --git a/core/src/gradient.rs b/core/src/gradient.rs index 4711b04452..ccae0bcef7 100644 --- a/core/src/gradient.rs +++ b/core/src/gradient.rs @@ -12,17 +12,13 @@ pub enum Gradient { } impl Gradient { - /// Adjust the opacity of the gradient by a multiplier applied to each color stop. - pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self { - match &mut self { + /// Scales the alpha channel of the [`Gradient`] by the given factor. + pub fn scale_alpha(self, factor: f32) -> Self { + match self { Gradient::Linear(linear) => { - for stop in linear.stops.iter_mut().flatten() { - stop.color.a *= alpha_multiplier; - } + Gradient::Linear(linear.scale_alpha(factor)) } } - - self } } @@ -100,4 +96,14 @@ impl Linear { self } + + /// Scales the alpha channel of the [`Linear`] gradient by the given + /// factor. + pub fn scale_alpha(mut self, factor: f32) -> Self { + for stop in self.stops.iter_mut().flatten() { + stop.color.a *= factor; + } + + self + } } diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index 479fe6fb73..e531510f79 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; bitflags! { /// The current state of the keyboard modifiers. - #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Modifiers: u32{ /// The "shift" key. const SHIFT = 0b100; diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 40bd71232e..dcb4d8de7d 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -80,14 +80,9 @@ where let mut fill_main_sum = 0; let mut cross = match axis { - Axis::Horizontal => match height { - Length::Shrink => 0.0, - _ => max_cross, - }, - Axis::Vertical => match width { - Length::Shrink => 0.0, - _ => max_cross, - }, + Axis::Vertical if width == Length::Shrink => 0.0, + Axis::Horizontal if height == Length::Shrink => 0.0, + _ => max_cross, }; let mut available = axis.main(limits.max()) - total_spacing; @@ -103,35 +98,14 @@ where }; if fill_main_factor == 0 { - if fill_cross_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = - child.as_widget().layout(tree, renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - cross = cross.max(axis.cross(size)); - - nodes[i] = layout; - } - } else { - fill_main_sum += fill_main_factor; - } - } - - for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let (fill_main_factor, fill_cross_factor) = { - let size = child.as_widget().size(); - - axis.pack(size.width.fill_factor(), size.height.fill_factor()) - }; - - if fill_main_factor == 0 && fill_cross_factor != 0 { - let (max_width, max_height) = axis.pack(available, cross); + let (max_width, max_height) = axis.pack( + available, + if fill_cross_factor == 0 { + max_cross + } else { + cross + }, + ); let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); @@ -141,9 +115,11 @@ where let size = layout.size(); available -= axis.main(size); - cross = cross.max(axis.cross(layout.size())); + cross = cross.max(axis.cross(size)); nodes[i] = layout; + } else { + fill_main_sum += fill_main_factor; } } @@ -175,14 +151,15 @@ where max_main }; - let max_cross = if fill_cross_factor == 0 { - max_cross - } else { - cross - }; - let (min_width, min_height) = axis.pack(min_main, 0.0); - let (max_width, max_height) = axis.pack(max_main, max_cross); + let (max_width, max_height) = axis.pack( + max_main, + if fill_cross_factor == 0 { + max_cross + } else { + cross + }, + ); let child_limits = Limits::new( Size::new(min_width, min_height), diff --git a/core/src/length.rs b/core/src/length.rs index 4c139895e6..5f24169f1d 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -48,12 +48,21 @@ impl Length { /// Specifically: /// - [`Length::Shrink`] if [`Length::Shrink`] or [`Length::Fixed`]. /// - [`Length::Fill`] otherwise. - pub fn fluid(&self) -> Length { + pub fn fluid(&self) -> Self { match self { Length::Fill | Length::FillPortion(_) => Length::Fill, Length::Shrink | Length::Fixed(_) => Length::Shrink, } } + + /// Adapts the [`Length`] so it can contain the other [`Length`] and + /// match its fluidity. + pub fn enclose(self, other: Length) -> Self { + match (self, other) { + (Length::Shrink, Length::Fill | Length::FillPortion(_)) => other, + _ => self, + } + } } impl From for Length { diff --git a/core/src/lib.rs b/core/src/lib.rs index 002336ee5f..d076413ec5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -30,6 +30,7 @@ pub mod overlay; pub mod renderer; pub mod svg; pub mod text; +pub mod theme; pub mod time; pub mod touch; pub mod widget; @@ -76,6 +77,7 @@ pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; pub use text::Text; +pub use theme::Theme; pub use transformation::Transformation; pub use vector::Vector; pub use widget::Widget; diff --git a/core/src/size.rs b/core/src/size.rs index 90e50d1390..267fc90e38 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -53,20 +53,20 @@ impl Size { } } -impl From<[f32; 2]> for Size { - fn from([width, height]: [f32; 2]) -> Self { +impl From<[T; 2]> for Size { + fn from([width, height]: [T; 2]) -> Self { Size { width, height } } } -impl From<[u16; 2]> for Size { - fn from([width, height]: [u16; 2]) -> Self { - Size::new(width.into(), height.into()) +impl From<(T, T)> for Size { + fn from((width, height): (T, T)) -> Self { + Self { width, height } } } -impl From> for Size { - fn from(vector: Vector) -> Self { +impl From> for Size { + fn from(vector: Vector) -> Self { Size { width: vector.x, height: vector.y, @@ -74,20 +74,23 @@ impl From> for Size { } } -impl From for [f32; 2] { - fn from(size: Size) -> [f32; 2] { +impl From> for [T; 2] { + fn from(size: Size) -> Self { [size.width, size.height] } } -impl From for Vector { - fn from(size: Size) -> Self { +impl From> for Vector { + fn from(size: Size) -> Self { Vector::new(size.width, size.height) } } -impl std::ops::Sub for Size { - type Output = Size; +impl std::ops::Sub for Size +where + T: std::ops::Sub, +{ + type Output = Size; fn sub(self, rhs: Self) -> Self::Output { Size { diff --git a/core/src/theme.rs b/core/src/theme.rs new file mode 100644 index 0000000000..948aaf83a2 --- /dev/null +++ b/core/src/theme.rs @@ -0,0 +1,227 @@ +//! Use the built-in theme and styles. +pub mod palette; + +pub use palette::Palette; + +use std::fmt; +use std::sync::Arc; + +/// A built-in theme. +#[derive(Debug, Clone, PartialEq, Default)] +pub enum Theme { + /// The built-in light variant. + #[default] + Light, + /// The built-in dark variant. + Dark, + /// The built-in Dracula variant. + Dracula, + /// The built-in Nord variant. + Nord, + /// The built-in Solarized Light variant. + SolarizedLight, + /// The built-in Solarized Dark variant. + SolarizedDark, + /// The built-in Gruvbox Light variant. + GruvboxLight, + /// The built-in Gruvbox Dark variant. + GruvboxDark, + /// The built-in Catppuccin Latte variant. + CatppuccinLatte, + /// The built-in Catppuccin Frappé variant. + CatppuccinFrappe, + /// The built-in Catppuccin Macchiato variant. + CatppuccinMacchiato, + /// The built-in Catppuccin Mocha variant. + CatppuccinMocha, + /// The built-in Tokyo Night variant. + TokyoNight, + /// The built-in Tokyo Night Storm variant. + TokyoNightStorm, + /// The built-in Tokyo Night Light variant. + TokyoNightLight, + /// The built-in Kanagawa Wave variant. + KanagawaWave, + /// The built-in Kanagawa Dragon variant. + KanagawaDragon, + /// The built-in Kanagawa Lotus variant. + KanagawaLotus, + /// The built-in Moonfly variant. + Moonfly, + /// The built-in Nightfly variant. + Nightfly, + /// The built-in Oxocarbon variant. + Oxocarbon, + /// The built-in Ferra variant: + Ferra, + /// A [`Theme`] that uses a [`Custom`] palette. + Custom(Arc), +} + +impl Theme { + /// A list with all the defined themes. + pub const ALL: &'static [Self] = &[ + Self::Light, + Self::Dark, + Self::Dracula, + Self::Nord, + Self::SolarizedLight, + Self::SolarizedDark, + Self::GruvboxLight, + Self::GruvboxDark, + Self::CatppuccinLatte, + Self::CatppuccinFrappe, + Self::CatppuccinMacchiato, + Self::CatppuccinMocha, + Self::TokyoNight, + Self::TokyoNightStorm, + Self::TokyoNightLight, + Self::KanagawaWave, + Self::KanagawaDragon, + Self::KanagawaLotus, + Self::Moonfly, + Self::Nightfly, + Self::Oxocarbon, + Self::Ferra, + ]; + + /// Creates a new custom [`Theme`] from the given [`Palette`]. + pub fn custom(name: String, palette: Palette) -> Self { + Self::custom_with_fn(name, palette, palette::Extended::generate) + } + + /// Creates a new custom [`Theme`] from the given [`Palette`], with + /// a custom generator of a [`palette::Extended`]. + pub fn custom_with_fn( + name: String, + palette: Palette, + generate: impl FnOnce(Palette) -> palette::Extended, + ) -> Self { + Self::Custom(Arc::new(Custom::with_fn(name, palette, generate))) + } + + /// Returns the [`Palette`] of the [`Theme`]. + pub fn palette(&self) -> Palette { + match self { + Self::Light => Palette::LIGHT, + Self::Dark => Palette::DARK, + Self::Dracula => Palette::DRACULA, + Self::Nord => Palette::NORD, + Self::SolarizedLight => Palette::SOLARIZED_LIGHT, + Self::SolarizedDark => Palette::SOLARIZED_DARK, + Self::GruvboxLight => Palette::GRUVBOX_LIGHT, + Self::GruvboxDark => Palette::GRUVBOX_DARK, + Self::CatppuccinLatte => Palette::CATPPUCCIN_LATTE, + Self::CatppuccinFrappe => Palette::CATPPUCCIN_FRAPPE, + Self::CatppuccinMacchiato => Palette::CATPPUCCIN_MACCHIATO, + Self::CatppuccinMocha => Palette::CATPPUCCIN_MOCHA, + Self::TokyoNight => Palette::TOKYO_NIGHT, + Self::TokyoNightStorm => Palette::TOKYO_NIGHT_STORM, + Self::TokyoNightLight => Palette::TOKYO_NIGHT_LIGHT, + Self::KanagawaWave => Palette::KANAGAWA_WAVE, + Self::KanagawaDragon => Palette::KANAGAWA_DRAGON, + Self::KanagawaLotus => Palette::KANAGAWA_LOTUS, + Self::Moonfly => Palette::MOONFLY, + Self::Nightfly => Palette::NIGHTFLY, + Self::Oxocarbon => Palette::OXOCARBON, + Self::Ferra => Palette::FERRA, + Self::Custom(custom) => custom.palette, + } + } + + /// Returns the [`palette::Extended`] of the [`Theme`]. + pub fn extended_palette(&self) -> &palette::Extended { + match self { + Self::Light => &palette::EXTENDED_LIGHT, + Self::Dark => &palette::EXTENDED_DARK, + Self::Dracula => &palette::EXTENDED_DRACULA, + Self::Nord => &palette::EXTENDED_NORD, + Self::SolarizedLight => &palette::EXTENDED_SOLARIZED_LIGHT, + Self::SolarizedDark => &palette::EXTENDED_SOLARIZED_DARK, + Self::GruvboxLight => &palette::EXTENDED_GRUVBOX_LIGHT, + Self::GruvboxDark => &palette::EXTENDED_GRUVBOX_DARK, + Self::CatppuccinLatte => &palette::EXTENDED_CATPPUCCIN_LATTE, + Self::CatppuccinFrappe => &palette::EXTENDED_CATPPUCCIN_FRAPPE, + Self::CatppuccinMacchiato => { + &palette::EXTENDED_CATPPUCCIN_MACCHIATO + } + Self::CatppuccinMocha => &palette::EXTENDED_CATPPUCCIN_MOCHA, + Self::TokyoNight => &palette::EXTENDED_TOKYO_NIGHT, + Self::TokyoNightStorm => &palette::EXTENDED_TOKYO_NIGHT_STORM, + Self::TokyoNightLight => &palette::EXTENDED_TOKYO_NIGHT_LIGHT, + Self::KanagawaWave => &palette::EXTENDED_KANAGAWA_WAVE, + Self::KanagawaDragon => &palette::EXTENDED_KANAGAWA_DRAGON, + Self::KanagawaLotus => &palette::EXTENDED_KANAGAWA_LOTUS, + Self::Moonfly => &palette::EXTENDED_MOONFLY, + Self::Nightfly => &palette::EXTENDED_NIGHTFLY, + Self::Oxocarbon => &palette::EXTENDED_OXOCARBON, + Self::Ferra => &palette::EXTENDED_FERRA, + Self::Custom(custom) => &custom.extended, + } + } +} + +impl fmt::Display for Theme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Light => write!(f, "Light"), + Self::Dark => write!(f, "Dark"), + Self::Dracula => write!(f, "Dracula"), + Self::Nord => write!(f, "Nord"), + Self::SolarizedLight => write!(f, "Solarized Light"), + Self::SolarizedDark => write!(f, "Solarized Dark"), + Self::GruvboxLight => write!(f, "Gruvbox Light"), + Self::GruvboxDark => write!(f, "Gruvbox Dark"), + Self::CatppuccinLatte => write!(f, "Catppuccin Latte"), + Self::CatppuccinFrappe => write!(f, "Catppuccin Frappé"), + Self::CatppuccinMacchiato => write!(f, "Catppuccin Macchiato"), + Self::CatppuccinMocha => write!(f, "Catppuccin Mocha"), + Self::TokyoNight => write!(f, "Tokyo Night"), + Self::TokyoNightStorm => write!(f, "Tokyo Night Storm"), + Self::TokyoNightLight => write!(f, "Tokyo Night Light"), + Self::KanagawaWave => write!(f, "Kanagawa Wave"), + Self::KanagawaDragon => write!(f, "Kanagawa Dragon"), + Self::KanagawaLotus => write!(f, "Kanagawa Lotus"), + Self::Moonfly => write!(f, "Moonfly"), + Self::Nightfly => write!(f, "Nightfly"), + Self::Oxocarbon => write!(f, "Oxocarbon"), + Self::Ferra => write!(f, "Ferra"), + Self::Custom(custom) => custom.fmt(f), + } + } +} + +/// A [`Theme`] with a customized [`Palette`]. +#[derive(Debug, Clone, PartialEq)] +pub struct Custom { + name: String, + palette: Palette, + extended: palette::Extended, +} + +impl Custom { + /// Creates a [`Custom`] theme from the given [`Palette`]. + pub fn new(name: String, palette: Palette) -> Self { + Self::with_fn(name, palette, palette::Extended::generate) + } + + /// Creates a [`Custom`] theme from the given [`Palette`] with + /// a custom generator of a [`palette::Extended`]. + pub fn with_fn( + name: String, + palette: Palette, + generate: impl FnOnce(Palette) -> palette::Extended, + ) -> Self { + Self { + name, + palette, + extended: generate(palette), + } + } +} + +impl fmt::Display for Custom { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/style/src/theme/palette.rs b/core/src/theme/palette.rs similarity index 97% rename from style/src/theme/palette.rs rename to core/src/theme/palette.rs index 15a964cd81..ca91c24821 100644 --- a/style/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -1,5 +1,5 @@ //! Define the colors of a theme. -use crate::core::{color, Color}; +use crate::{color, Color}; use once_cell::sync::Lazy; use palette::color_difference::Wcag21RelativeContrast; @@ -276,6 +276,17 @@ impl Palette { success: color!(0x00c15a), danger: color!(0xf62d0f), }; + + /// The built-in [Ferra] variant of a [`Palette`]. + /// + /// [Ferra]: https://github.com/casperstorm/ferra + pub const FERRA: Self = Self { + background: color!(0x2b292d), + text: color!(0xfecdb2), + primary: color!(0xd1d1e0), + success: color!(0xb1b695), + danger: color!(0xe06b75), + }; } /// An extended set of colors generated from a [`Palette`]. @@ -379,6 +390,10 @@ pub static EXTENDED_NIGHTFLY: Lazy = pub static EXTENDED_OXOCARBON: Lazy = Lazy::new(|| Extended::generate(Palette::OXOCARBON)); +/// The built-in Ferra variant of an [`Extended`] palette. +pub static EXTENDED_FERRA: Lazy = + Lazy::new(|| Extended::generate(Palette::FERRA)); + impl Extended { /// Generates an [`Extended`] palette from a simple [`Palette`]. pub fn generate(palette: Palette) -> Self { diff --git a/core/src/widget.rs b/core/src/widget.rs index 51326f1246..58a9f19be4 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -33,12 +33,12 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector}; /// - [`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.10/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.10/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.10/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.10/examples/geometry +/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.12/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.12/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.10/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.12/wgpu pub trait Widget where Renderer: crate::Renderer, diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 0796c4e4bb..66e2d066c6 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -6,7 +6,8 @@ use crate::renderer; use crate::text::{self, Paragraph}; use crate::widget::tree::{self, Tree}; use crate::{ - Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, + Widget, }; use std::borrow::Cow; @@ -17,7 +18,6 @@ pub use text::{LineHeight, Shaping}; #[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { content: Cow<'a, str>, @@ -29,16 +29,18 @@ where vertical_alignment: alignment::Vertical, font: Option, shaping: Shaping, - style: Theme::Style, + style: Style<'a, Theme>, } impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new(content: impl Into>) -> Self { + pub fn new(content: impl Into>) -> Self + where + Theme: DefaultStyle + 'a, + { Text { content: content.into(), size: None, @@ -49,7 +51,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Basic, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -74,8 +76,21 @@ where } /// Sets the style of the [`Text`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { + self.style = Box::new(style); + self + } + + /// Sets the [`Color`] of the [`Text`]. + pub fn color(self, color: impl Into) -> Self { + self.color_maybe(Some(color)) + } + + /// Sets the [`Color`] of the [`Text`], if `Some`. + pub fn color_maybe(mut self, color: Option>) -> Self { + let color = color.map(Into::into); + + self.style = Box::new(move |_theme| Appearance { color }); self } @@ -123,7 +138,6 @@ pub struct State(P); impl<'a, Message, Theme, Renderer> Widget for Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -174,15 +188,9 @@ where viewport: &Rectangle, ) { let state = tree.state.downcast_ref::>(); + let appearance = (self.style)(theme); - draw( - renderer, - style, - layout, - state, - theme.appearance(self.style.clone()), - viewport, - ); + draw(renderer, style, layout, state, appearance, viewport); } } @@ -273,7 +281,7 @@ pub fn draw( impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -283,30 +291,9 @@ where } } -impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer> -where - Theme: StyleSheet, - Renderer: text::Renderer, -{ - fn clone(&self) -> Self { - Self { - content: self.content.clone(), - size: self.size, - line_height: self.line_height, - width: self.width, - height: self.height, - horizontal_alignment: self.horizontal_alignment, - vertical_alignment: self.vertical_alignment, - font: self.font, - style: self.style.clone(), - shaping: self.shaping, - } - } -} - impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer> where - Theme: StyleSheet, + Theme: DefaultStyle + 'a, Renderer: text::Renderer, { fn from(content: &'a str) -> Self { @@ -317,7 +304,7 @@ where impl<'a, Message, Theme, Renderer> From<&'a str> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: DefaultStyle + 'a, Renderer: text::Renderer + 'a, { fn from(content: &'a str) -> Self { @@ -325,16 +312,7 @@ where } } -/// The style sheet of some text. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; - - /// Produces the [`Appearance`] of some text. - fn appearance(&self, style: Self::Style) -> Appearance; -} - -/// The apperance of some text. +/// The appearance of some text. #[derive(Debug, Clone, Copy, Default)] pub struct Appearance { /// The [`Color`] of the text. @@ -342,3 +320,24 @@ pub struct Appearance { /// The default, `None`, means using the inherited color. pub color: Option, } + +/// The style of some [`Text`]. +pub type Style<'a, Theme> = Box Appearance + 'a>; + +/// The default style of some [`Text`]. +pub trait DefaultStyle { + /// Returns the default style of some [`Text`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + Appearance::default() + } +} + +impl DefaultStyle for Color { + fn default_style(&self) -> Appearance { + Appearance { color: Some(*self) } + } +} diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs index 8a59e83c03..b0c000d6e9 100644 --- a/core/src/window/redraw_request.rs +++ b/core/src/window/redraw_request.rs @@ -13,7 +13,7 @@ pub enum RedrawRequest { #[cfg(test)] mod tests { use super::*; - use crate::time::{Duration, Instant}; + use crate::time::Duration; #[test] fn ordering() { diff --git a/docs/logo.svg b/docs/logo.svg index ff4eb3a7ff..aa1924c220 100644 --- a/docs/logo.svg +++ b/docs/logo.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 6a68cca1f3..4576404fde 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -1,20 +1,17 @@ use std::{f32::consts::PI, time::Instant}; -use iced::executor; use iced::mouse; use iced::widget::canvas::{ self, stroke, Cache, Canvas, Geometry, Path, Stroke, }; -use iced::{ - Application, Command, Element, Length, Point, Rectangle, Renderer, - Settings, Subscription, Theme, -}; +use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { - Arc::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program("Arc - Iced", Arc::update, Arc::view) + .subscription(Arc::subscription) + .theme(|_| Theme::Dark) + .antialiasing(true) + .run() } struct Arc { @@ -27,30 +24,9 @@ enum Message { Tick, } -impl Application for Arc { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Arc { - start: Instant::now(), - cache: Cache::default(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Arc - Iced") - } - - fn update(&mut self, _: Message) -> Command { +impl Arc { + fn update(&mut self, _: Message) { self.cache.clear(); - - Command::none() } fn view(&self) -> Element { @@ -60,16 +36,21 @@ impl Application for Arc { .into() } - fn theme(&self) -> Theme { - Theme::Dark - } - fn subscription(&self) -> Subscription { iced::time::every(std::time::Duration::from_millis(10)) .map(|_| Message::Tick) } } +impl Default for Arc { + fn default() -> Self { + Arc { + start: Instant::now(), + cache: Cache::default(), + } + } +} + impl canvas::Program for Arc { type State = (); diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 56cb23ba8b..cf70bd4012 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,12 +1,11 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::widget::{button, column, text}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program("Bezier Tool - Iced", Example::update, Example::view) + .antialiasing(true) + .run() } #[derive(Default)] @@ -21,17 +20,7 @@ enum Message { Clear, } -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Example::default() - } - - fn title(&self) -> String { - String::from("Bezier tool - Iced") - } - +impl Example { fn update(&mut self, message: Message) { match message { Message::AddCurve(curve) => { @@ -49,7 +38,9 @@ impl Sandbox for Example { 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), + button("Clear") + .style(button::danger) + .on_press(Message::Clear), ] .padding(20) .spacing(20) diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 834a8f5c37..3894933644 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -1,13 +1,12 @@ -use iced::executor; -use iced::font::{self, Font}; -use iced::theme; use iced::widget::{checkbox, column, container, row, text}; -use iced::{Application, Command, Element, Length, Settings, Theme}; +use iced::{Element, Font, Length}; const ICON_FONT: Font = Font::with_name("icons"); pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::program("Checkbox - Iced", Example::update, Example::view) + .font(include_bytes!("../fonts/icons.ttf").as_slice()) + .run() } #[derive(Default)] @@ -22,28 +21,10 @@ enum Message { DefaultToggled(bool), CustomToggled(bool), StyledToggled(bool), - FontLoaded(Result<(), font::Error>), } -impl Application for Example { - type Message = Message; - type Flags = (); - type Executor = executor::Default; - type Theme = Theme; - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self::default(), - font::load(include_bytes!("../fonts/icons.ttf").as_slice()) - .map(Message::FontLoaded), - ) - } - - fn title(&self) -> String { - String::from("Checkbox - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl Example { + fn update(&mut self, message: Message) { match message { Message::DefaultToggled(default) => { self.default = default; @@ -54,27 +35,23 @@ impl Application for Example { Message::CustomToggled(custom) => { self.custom = custom; } - Message::FontLoaded(_) => (), } - - Command::none() } fn view(&self) -> Element { let default_checkbox = checkbox("Default", self.default) .on_toggle(Message::DefaultToggled); - let styled_checkbox = |label, style| { + let styled_checkbox = |label| { checkbox(label, self.styled) .on_toggle_maybe(self.default.then_some(Message::StyledToggled)) - .style(style) }; let checkboxes = row![ - styled_checkbox("Primary", theme::Checkbox::Primary), - styled_checkbox("Secondary", theme::Checkbox::Secondary), - styled_checkbox("Success", theme::Checkbox::Success), - styled_checkbox("Danger", theme::Checkbox::Danger), + styled_checkbox("Primary").style(checkbox::primary), + styled_checkbox("Secondary").style(checkbox::secondary), + styled_checkbox("Success").style(checkbox::success), + styled_checkbox("Danger").style(checkbox::danger), ] .spacing(20); diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 1325252668..897f8f1b38 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,17 +1,18 @@ -use iced::executor; +use iced::alignment; use iced::mouse; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Renderer, - Settings, Subscription, Theme, Vector, + Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Theme, Vector, }; pub fn main() -> iced::Result { - Clock::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program("Clock - Iced", Clock::update, Clock::view) + .subscription(Clock::subscription) + .theme(Clock::theme) + .antialiasing(true) + .run() } struct Clock { @@ -24,28 +25,8 @@ enum Message { Tick(time::OffsetDateTime), } -impl Application for Clock { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Clock { - now: time::OffsetDateTime::now_local() - .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), - clock: Cache::default(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Clock - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl Clock { + fn update(&mut self, message: Message) { match message { Message::Tick(local_time) => { let now = local_time; @@ -56,8 +37,6 @@ impl Application for Clock { } } } - - Command::none() } fn view(&self) -> Element { @@ -80,6 +59,21 @@ impl Application for Clock { ) }) } + + fn theme(&self) -> Theme { + Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()] + .clone() + } +} + +impl Default for Clock { + fn default() -> Self { + Self { + now: time::OffsetDateTime::now_local() + .unwrap_or_else(|_| time::OffsetDateTime::now_utc()), + clock: Cache::default(), + } + } } impl canvas::Program for Clock { @@ -89,16 +83,18 @@ impl canvas::Program for Clock { &self, _state: &Self::State, renderer: &Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec { let clock = self.clock.draw(renderer, bounds.size(), |frame| { + let palette = theme.extended_palette(); + let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; let background = Path::circle(center, radius); - frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); + frame.fill(&background, palette.secondary.strong.color); let short_hand = Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); @@ -111,7 +107,7 @@ impl canvas::Program for Clock { let thin_stroke = || -> Stroke { Stroke { width, - style: stroke::Style::Solid(Color::WHITE), + style: stroke::Style::Solid(palette.secondary.strong.text), line_cap: LineCap::Round, ..Stroke::default() } @@ -120,7 +116,7 @@ impl canvas::Program for Clock { let wide_stroke = || -> Stroke { Stroke { width: width * 3.0, - style: stroke::Style::Solid(Color::WHITE), + style: stroke::Style::Solid(palette.secondary.strong.text), line_cap: LineCap::Round, ..Stroke::default() } @@ -139,8 +135,31 @@ impl canvas::Program for Clock { }); frame.with_save(|frame| { - frame.rotate(hand_rotation(self.now.second(), 60)); + let rotation = hand_rotation(self.now.second(), 60); + + frame.rotate(rotation); frame.stroke(&long_hand, thin_stroke()); + + let rotate_factor = if rotation < 180.0 { 1.0 } else { -1.0 }; + + frame.rotate(Degrees(-90.0 * rotate_factor)); + frame.fill_text(canvas::Text { + content: theme.to_string(), + size: (radius / 15.0).into(), + position: Point::new( + (0.78 * radius) * rotate_factor, + -width * 2.0, + ), + color: palette.secondary.strong.text, + horizontal_alignment: if rotate_factor > 0.0 { + alignment::Horizontal::Right + } else { + alignment::Horizontal::Left + }, + vertical_alignment: alignment::Vertical::Bottom, + font: Font::MONOSPACE, + ..canvas::Text::default() + }); }); }); @@ -148,8 +167,8 @@ impl canvas::Program for Clock { } } -fn hand_rotation(n: u8, total: u8) -> f32 { +fn hand_rotation(n: u8, total: u8) -> Degrees { let turns = n as f32 / total as f32; - 2.0 * std::f32::consts::PI * turns + Degrees(360.0 * turns) } diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 2da6c6ed5c..bf9bff19ca 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["canvas", "palette"] +iced.features = ["canvas"] palette.workspace = true diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index a5fd46e070..d9325edb29 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -3,20 +3,23 @@ use iced::mouse; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox, - Settings, Size, Vector, -}; -use palette::{ - self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue, + Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size, + Vector, }; +use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue}; use std::marker::PhantomData; use std::ops::RangeInclusive; pub fn main() -> iced::Result { - ColorPalette::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program( + "Color Palette - Iced", + ColorPalette::update, + ColorPalette::view, + ) + .theme(ColorPalette::theme) + .default_font(Font::MONOSPACE) + .antialiasing(true) + .run() } #[derive(Default)] @@ -40,17 +43,7 @@ pub enum Message { LchColorChanged(palette::Lch), } -impl Sandbox for ColorPalette { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Color palette - Iced") - } - +impl ColorPalette { fn update(&mut self, message: Message) { let srgb = match message { Message::RgbColorChanged(rgb) => Rgb::from(rgb), @@ -87,6 +80,19 @@ impl Sandbox for ColorPalette { .spacing(10) .into() } + + fn theme(&self) -> iced::Theme { + iced::Theme::custom( + String::from("Custom"), + iced::theme::Palette { + background: self.theme.base, + primary: *self.theme.lower.first().unwrap(), + text: *self.theme.higher.last().unwrap(), + success: *self.theme.lower.last().unwrap(), + danger: *self.theme.higher.last().unwrap(), + }, + ) + } } #[derive(Debug)] @@ -150,7 +156,7 @@ impl Theme { .into() } - fn draw(&self, frame: &mut Frame) { + fn draw(&self, frame: &mut Frame, text_color: Color) { let pad = 20.0; let box_size = Size { @@ -169,6 +175,7 @@ impl Theme { horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Top, size: Pixels(15.0), + color: text_color, ..canvas::Text::default() }; @@ -246,12 +253,14 @@ impl canvas::Program for Theme { &self, _state: &Self::State, renderer: &Renderer, - _theme: &iced::Theme, + theme: &iced::Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec { let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| { - self.draw(frame); + let palette = theme.extended_palette(); + + self.draw(frame, palette.background.base.text); }); vec![theme] @@ -308,7 +317,7 @@ impl ColorPicker { 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(185).size(14), + text(color.to_string()).width(185).size(12), ] .spacing(10) .align_items(Alignment::Center) diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index 4f347667e2..2feb4522ab 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -1,10 +1,10 @@ use iced::widget::{ column, combo_box, container, scrollable, text, vertical_space, }; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Combo Box - Iced", Example::update, Example::view) } struct Example { @@ -20,9 +20,7 @@ enum Message { Closed, } -impl Sandbox for Example { - type Message = Message; - +impl Example { fn new() -> Self { Self { languages: combo_box::State::new(Language::ALL.to_vec()), @@ -31,10 +29,6 @@ impl Sandbox for Example { } } - fn title(&self) -> String { - String::from("Combo box - Iced") - } - fn update(&mut self, message: Message) { match message { Message::Selected(language) => { @@ -68,7 +62,7 @@ impl Sandbox for Example { text(&self.text), "What is your language?", combo_box, - vertical_space(150), + vertical_space().height(150), ] .width(Length::Fill) .align_items(Alignment::Center) @@ -83,6 +77,12 @@ impl Sandbox for Example { } } +impl Default for Example { + fn default() -> Self { + Example::new() + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Language { Danish, diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 81be4d7f03..43ba318741 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -1,10 +1,10 @@ use iced::widget::container; -use iced::{Element, Length, Sandbox, Settings}; +use iced::{Element, Length}; use numeric_input::numeric_input; pub fn main() -> iced::Result { - Component::run(Settings::default()) + iced::run("Component - Iced", Component::update, Component::view) } #[derive(Default)] @@ -17,17 +17,7 @@ enum Message { NumericInputChanged(Option), } -impl Sandbox for Component { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Component - Iced") - } - +impl Component { fn update(&mut self, message: Message) { match message { Message::NumericInputChanged(value) => { @@ -48,7 +38,7 @@ impl Sandbox for Component { mod numeric_input { use iced::alignment::{self, Alignment}; use iced::widget::{button, component, row, text, text_input, Component}; - use iced::{Element, Length}; + use iced::{Element, Length, Size}; pub struct NumericInput { value: Option, @@ -81,7 +71,13 @@ mod numeric_input { } } - impl Component for NumericInput { + impl Component for NumericInput + where + Theme: text::DefaultStyle + + button::DefaultStyle + + text_input::DefaultStyle + + 'static, + { type State = (); type Event = Event; @@ -111,7 +107,7 @@ mod numeric_input { } } - fn view(&self, _state: &Self::State) -> Element { + fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> { let button = |label, on_press| { button( text(label) @@ -143,10 +139,22 @@ mod numeric_input { .spacing(10) .into() } + + fn size_hint(&self) -> Size { + Size { + width: Length::Fill, + height: Length::Shrink, + } + } } - impl<'a, Message> From> for Element<'a, Message> + impl<'a, Message, Theme> From> + for Element<'a, Message, Theme> where + Theme: text::DefaultStyle + + button::DefaultStyle + + text_input::DefaultStyle + + 'static, Message: 'a, { fn from(numeric_input: NumericInput) -> Self { diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 13dcbf8615..0dd7a97636 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,50 +1,40 @@ -use iced::widget::{button, column, text}; -use iced::{Alignment, Element, Sandbox, Settings}; +use iced::widget::{button, column, text, Column}; +use iced::Alignment; pub fn main() -> iced::Result { - Counter::run(Settings::default()) + iced::run("A cool counter", Counter::update, Counter::view) } +#[derive(Default)] struct Counter { - value: i32, + value: i64, } #[derive(Debug, Clone, Copy)] enum Message { - IncrementPressed, - DecrementPressed, + Increment, + Decrement, } -impl Sandbox for Counter { - type Message = Message; - - fn new() -> Self { - Self { value: 0 } - } - - fn title(&self) -> String { - String::from("Counter - Iced") - } - +impl Counter { fn update(&mut self, message: Message) { match message { - Message::IncrementPressed => { + Message::Increment => { self.value += 1; } - Message::DecrementPressed => { + Message::Decrement => { self.value -= 1; } } } - fn view(&self) -> Element { + fn view(&self) -> Column { column![ - button("Increment").on_press(Message::IncrementPressed), + button("Increment").on_press(Message::Increment), text(self.value).size(50), - button("Decrement").on_press(Message::DecrementPressed) + button("Decrement").on_press(Message::Decrement) ] .padding(20) .align_items(Alignment::Center) - .into() } } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index f64379fa1b..c093e240ea 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -82,12 +82,10 @@ mod quad { } use iced::widget::{column, container, slider, text}; -use iced::{ - Alignment, Color, Element, Length, Sandbox, Settings, Shadow, Vector, -}; +use iced::{Alignment, Color, Element, Length, Shadow, Vector}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Custom Quad - Iced", Example::update, Example::view) } struct Example { @@ -109,9 +107,7 @@ enum Message { ShadowBlurRadiusChanged(f32), } -impl Sandbox for Example { - type Message = Message; - +impl Example { fn new() -> Self { Self { radius: [50.0; 4], @@ -124,10 +120,6 @@ impl Sandbox for Example { } } - fn title(&self) -> String { - String::from("Custom widget - Iced") - } - fn update(&mut self, message: Message) { let [tl, tr, br, bl] = self.radius; match message { @@ -203,3 +195,9 @@ impl Sandbox for Example { .into() } } + +impl Default for Example { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 9e8da3baf5..aa3dafe9b5 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -2,18 +2,16 @@ mod scene; use scene::Scene; -use iced::executor; use iced::time::Instant; use iced::widget::shader::wgpu; use iced::widget::{checkbox, column, container, row, shader, slider, text}; use iced::window; -use iced::{ - Alignment, Application, Color, Command, Element, Length, Subscription, - Theme, -}; +use iced::{Alignment, Color, Element, Length, Subscription}; fn main() -> iced::Result { - IcedCubes::run(iced::Settings::default()) + iced::program("Custom Shader - Iced", IcedCubes::update, IcedCubes::view) + .subscription(IcedCubes::subscription) + .run() } struct IcedCubes { @@ -30,27 +28,15 @@ enum Message { LightColorChanged(Color), } -impl Application for IcedCubes { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self { - start: Instant::now(), - scene: Scene::new(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - "Iced Cubes".to_string() +impl IcedCubes { + fn new() -> Self { + Self { + start: Instant::now(), + scene: Scene::new(), + } } - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Message) { match message { Message::CubeAmountChanged(amount) => { self.scene.change_amount(amount); @@ -68,11 +54,9 @@ impl Application for IcedCubes { self.scene.light_color = color; } } - - Command::none() } - fn view(&self) -> Element<'_, Self::Message> { + fn view(&self) -> Element<'_, Message> { let top_controls = row![ control( "Amount", @@ -147,11 +131,17 @@ impl Application for IcedCubes { .into() } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { window::frames().map(Message::Tick) } } +impl Default for IcedCubes { + fn default() -> Self { + Self::new() + } +} + fn control<'a>( label: &'static str, control: impl Into>, diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 25c0bb3943..aa49ebd0ef 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -62,7 +62,7 @@ mod circle { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border: Border::with_radius(self.radius), + border: Border::rounded(self.radius), ..renderer::Quad::default() }, Color::BLACK, @@ -83,10 +83,10 @@ mod circle { use circle::circle; use iced::widget::{column, container, slider, text}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Custom Widget - Iced", Example::update, Example::view) } struct Example { @@ -98,17 +98,11 @@ enum Message { RadiusChanged(f32), } -impl Sandbox for Example { - type Message = Message; - +impl Example { fn new() -> Self { Example { radius: 50.0 } } - fn title(&self) -> String { - String::from("Custom widget - Iced") - } - fn update(&mut self, message: Message) { match message { Message::RadiusChanged(radius) => { @@ -136,3 +130,9 @@ impl Sandbox for Example { .into() } } + +impl Default for Example { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 675e9e2672..9f4769e009 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -1,14 +1,12 @@ -use iced::executor; -use iced::widget::{button, column, container, progress_bar, text, Column}; -use iced::{ - Alignment, Application, Command, Element, Length, Settings, Subscription, - Theme, -}; - mod download; +use iced::widget::{button, column, container, progress_bar, text, Column}; +use iced::{Alignment, Element, Length, Subscription}; + pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::program("Download Progress - Iced", Example::update, Example::view) + .subscription(Example::subscription) + .run() } #[derive(Debug)] @@ -24,27 +22,15 @@ pub enum Message { DownloadProgressed((usize, download::Progress)), } -impl Application for Example { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Example, Command) { - ( - Example { - downloads: vec![Download::new(0)], - last_id: 0, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Download progress - Iced") +impl Example { + fn new() -> Self { + Self { + downloads: vec![Download::new(0)], + last_id: 0, + } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) { match message { Message::Add => { self.last_id += 1; @@ -63,9 +49,7 @@ impl Application for Example { download.progress(progress); } } - }; - - Command::none() + } } fn subscription(&self) -> Subscription { @@ -93,6 +77,12 @@ impl Application for Example { } } +impl Default for Example { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug)] struct Download { id: usize, diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 75b6626445..ed16018a5c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,15 +1,10 @@ -use iced::executor; use iced::highlighter::{self, Highlighter}; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{ - Alignment, Application, Command, Element, Font, Length, Settings, - Subscription, -}; +use iced::{Alignment, Command, Element, Font, Length, Subscription, Theme}; use std::ffi; use std::io; @@ -17,11 +12,13 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub fn main() -> iced::Result { - Editor::run(Settings { - fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font::MONOSPACE, - ..Settings::default() - }) + iced::program("Editor - Iced", Editor::update, Editor::view) + .load(Editor::load) + .subscription(Editor::subscription) + .theme(Editor::theme) + .font(include_bytes!("../fonts/icons.ttf").as_slice()) + .default_font(Font::MONOSPACE) + .run() } struct Editor { @@ -43,27 +40,22 @@ enum Message { FileSaved(Result), } -impl Application for Editor { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self { - file: None, - content: text_editor::Content::new(), - theme: highlighter::Theme::SolarizedDark, - is_loading: true, - is_dirty: false, - }, - Command::perform(load_file(default_file()), Message::FileOpened), - ) +impl Editor { + fn new() -> Self { + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + } } - fn title(&self) -> String { - String::from("Editor - Iced") + fn load() -> Command { + Command::perform( + load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))), + Message::FileOpened, + ) } fn update(&mut self, message: Message) -> Command { @@ -155,7 +147,7 @@ impl Application for Editor { "Save file", self.is_dirty.then_some(Message::SaveFile) ), - horizontal_space(Length::Fill), + horizontal_space(), pick_list( highlighter::Theme::ALL, Some(self.theme), @@ -179,7 +171,7 @@ impl Application for Editor { } else { String::from("New file") }), - horizontal_space(Length::Fill), + horizontal_space(), text({ let (line, column) = self.content.cursor_position(); @@ -222,16 +214,18 @@ impl Application for Editor { } } +impl Default for Editor { + fn default() -> Self { + Self::new() + } +} + #[derive(Debug, Clone)] pub enum Error { DialogClosed, IoError(io::ErrorKind), } -fn default_file() -> PathBuf { - PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))) -} - async fn open_file() -> Result<(PathBuf, Arc), Error> { let picked_file = rfd::AsyncFileDialog::new() .set_title("Open a text file...") @@ -239,10 +233,14 @@ async fn open_file() -> Result<(PathBuf, Arc), Error> { .await .ok_or(Error::DialogClosed)?; - load_file(picked_file.path().to_owned()).await + load_file(picked_file).await } -async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc), Error> { +async fn load_file( + path: impl Into, +) -> Result<(PathBuf, Arc), Error> { + let path = path.into(); + let contents = tokio::fs::read_to_string(&path) .await .map(Arc::new) @@ -287,10 +285,10 @@ fn action<'a, Message: Clone + 'a>( label, tooltip::Position::FollowCursor, ) - .style(theme::Container::Box) + .style(container::rounded_box) .into() } else { - action.style(theme::Button::Secondary).into() + action.style(button::secondary).into() } } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index d5d496c7c7..bf568c945e 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,21 +1,14 @@ use iced::alignment; use iced::event::{self, Event}; -use iced::executor; use iced::widget::{button, checkbox, container, text, Column}; use iced::window; -use iced::{ - Alignment, Application, Command, Element, Length, Settings, Subscription, - Theme, -}; +use iced::{Alignment, Command, Element, Length, Subscription}; pub fn main() -> iced::Result { - Events::run(Settings { - window: window::Settings { - exit_on_close_request: false, - ..window::Settings::default() - }, - ..Settings::default() - }) + iced::program("Events - Iced", Events::update, Events::view) + .subscription(Events::subscription) + .exit_on_close_request(false) + .run() } #[derive(Debug, Default)] @@ -31,20 +24,7 @@ enum Message { Exit, } -impl Application for Events { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Events, Command) { - (Events::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Events - Iced") - } - +impl Events { fn update(&mut self, message: Message) -> Command { match message { Message::EventOccurred(event) if self.enabled => { diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index ec618dc140..7bed272df2 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,10 +1,9 @@ -use iced::executor; use iced::widget::{button, column, container}; use iced::window; -use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; +use iced::{Alignment, Command, Element, Length}; pub fn main() -> iced::Result { - Exit::run(Settings::default()) + iced::program("Exit - Iced", Exit::update, Exit::view).run() } #[derive(Default)] @@ -18,20 +17,7 @@ enum Message { Exit, } -impl Application for Exit { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - (Self::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Exit - Iced") - } - +impl Exit { fn update(&mut self, message: Message) -> Command { match message { Message::Confirm => window::close(window::Id::MAIN), diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9cbb7fff2e..2b0fae0bc7 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -5,32 +5,24 @@ mod preset; use grid::Grid; use preset::Preset; -use iced::executor; -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, Command, Element, Length, Settings, Subscription, -}; +use iced::{Alignment, Command, Element, Length, Subscription, Theme}; use std::time::Duration; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - GameOfLife::run(Settings { - antialiasing: true, - window: window::Settings { - position: window::Position::Centered, - ..window::Settings::default() - }, - ..Settings::default() - }) + iced::program("Game of Life - Iced", GameOfLife::update, GameOfLife::view) + .subscription(GameOfLife::subscription) + .theme(|_| Theme::Dark) + .antialiasing(true) + .centered() + .run() } -#[derive(Default)] struct GameOfLife { grid: Grid, is_playing: bool, @@ -52,24 +44,16 @@ enum Message { PresetPicked(Preset), } -impl Application for GameOfLife { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Self { - speed: 5, - ..Self::default() - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Game of Life - Iced") +impl GameOfLife { + fn new() -> Self { + Self { + grid: Grid::default(), + is_playing: false, + queued_ticks: 0, + speed: 5, + next_speed: None, + version: 0, + } } fn update(&mut self, message: Message) -> Command { @@ -154,9 +138,11 @@ impl Application for GameOfLife { .height(Length::Fill) .into() } +} - fn theme(&self) -> Theme { - Theme::Dark +impl Default for GameOfLife { + fn default() -> Self { + Self::new() } } @@ -171,7 +157,7 @@ fn view_controls<'a>( .on_press(Message::TogglePlayback), button("Next") .on_press(Message::Next) - .style(theme::Button::Secondary), + .style(button::secondary), ] .spacing(10); @@ -185,17 +171,14 @@ fn view_controls<'a>( row![ playback_controls, speed_controls, - checkbox("Grid", is_grid_enabled) - .on_toggle(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), + checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid), + row![ + pick_list(preset::ALL, Some(preset), Message::PresetPicked), + button("Clear") + .on_press(Message::Clear) + .style(button::danger) + ] + .spacing(10) ] .padding(10) .spacing(20) diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 1ccc4dd69a..63efcbdd85 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -147,51 +147,35 @@ mod rainbow { } use iced::widget::{column, container, scrollable}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::{Element, Length}; use rainbow::rainbow; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Custom 2D Geometry - Iced", |_: &mut _, _| {}, view) } -struct Example; - -impl Sandbox for Example { - type Message = (); - - fn new() -> Self { - Self - } - - fn title(&self) -> String { - String::from("Custom 2D geometry - Iced") - } - - fn update(&mut self, _: ()) {} - - fn view(&self) -> Element<()> { - let content = column![ - rainbow(), - "In this example we draw a custom widget Rainbow, using \ +fn view(_state: &()) -> 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.", - "Move your cursor over it, and see the center vertex \ + "Move your cursor over it, and see the center vertex \ follow you!", - "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); - - let scrollable = - scrollable(container(content).width(Length::Fill).center_x()); - - container(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() - } + ] + .padding(20) + .spacing(20) + .max_width(500); + + let scrollable = + scrollable(container(content).width(Length::Fill).center_x()); + + container(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 32b2e4aaa6..22c21cdd66 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,23 +1,17 @@ -use iced::application; -use iced::theme::{self, Theme}; +use iced::gradient; +use iced::program; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; -use iced::{gradient, window}; -use iced::{ - Alignment, Background, Color, Element, Length, Radians, Sandbox, Settings, -}; +use iced::{Alignment, Color, Element, Length, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - Gradient::run(Settings { - window: window::Settings { - transparent: true, - ..Default::default() - }, - ..Default::default() - }) + iced::program("Gradient - Iced", Gradient::update, Gradient::view) + .style(Gradient::style) + .transparent(true) + .run() } #[derive(Debug, Clone, Copy)] @@ -36,9 +30,7 @@ enum Message { TransparentToggled(bool), } -impl Sandbox for Gradient { - type Message = Message; - +impl Gradient { fn new() -> Self { Self { start: Color::WHITE, @@ -48,10 +40,6 @@ impl Sandbox for Gradient { } } - fn title(&self) -> String { - String::from("Gradient") - } - fn update(&mut self, message: Message) { match message { Message::StartChanged(color) => self.start = color, @@ -71,20 +59,16 @@ impl Sandbox for Gradient { transparent, } = *self; - let gradient_box = container(horizontal_space(Length::Fill)) - .width(Length::Fill) - .height(Length::Fill) - .style(move |_: &_| { + let gradient_box = container(horizontal_space()) + .style(move |_theme, _status| { let gradient = gradient::Linear::new(angle) .add_stop(0.0, start) - .add_stop(1.0, end) - .into(); + .add_stop(1.0, end); - container::Appearance { - background: Some(Background::Gradient(gradient)), - ..Default::default() - } - }); + gradient.into() + }) + .width(Length::Fill) + .height(Length::Fill); let angle_picker = row![ text("Angle").width(64), @@ -111,20 +95,26 @@ impl Sandbox for Gradient { .into() } - fn style(&self) -> theme::Application { + fn style(&self, theme: &Theme) -> program::Appearance { + use program::DefaultStyle; + if self.transparent { - theme::Application::custom(|theme: &Theme| { - application::Appearance { - background_color: Color::TRANSPARENT, - text_color: theme.palette().text, - } - }) + program::Appearance { + background_color: Color::TRANSPARENT, + text_color: theme.palette().text, + } } else { - theme::Application::Default + Theme::default_style(theme) } } } +impl Default for Gradient { + fn default() -> Self { + Self::new() + } +} + fn color_picker(label: &str, color: Color) -> Element<'_, Color> { row![ text(label).width(64), diff --git a/examples/integration/README.md b/examples/integration/README.md index 996cdc17ce..aa3a6e94ad 100644 --- a/examples/integration/README.md +++ b/examples/integration/README.md @@ -14,7 +14,7 @@ cargo run --package integration_wgpu ``` ### How to run this example with WebGL backend -NOTE: Currently, WebGL backend is is still experimental, so expect bugs. +NOTE: Currently, WebGL backend is still experimental, so expect bugs. ```sh # 0. Install prerequisites diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab8284f..28050f8ad4 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,25 +1,25 @@ use iced_wgpu::Renderer; -use iced_widget::{slider, text_input, Column, Row, Text}; -use iced_winit::core::{Alignment, Color, Element, Length}; +use iced_widget::{column, container, row, slider, text, text_input}; +use iced_winit::core::alignment; +use iced_winit::core::{Color, Element, Length, Theme}; use iced_winit::runtime::{Command, Program}; -use iced_winit::style::Theme; pub struct Controls { background_color: Color, - text: String, + input: String, } #[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), - TextChanged(String), + InputChanged(String), } impl Controls { pub fn new() -> Controls { Controls { background_color: Color::BLACK, - text: String::default(), + input: String::default(), } } @@ -38,8 +38,8 @@ impl Program for Controls { Message::BackgroundColorChanged(color) => { self.background_color = color; } - Message::TextChanged(text) => { - self.text = text; + Message::InputChanged(input) => { + self.input = input; } } @@ -48,60 +48,48 @@ impl Program for Controls { fn view(&self) -> Element { let background_color = self.background_color; - let text = &self.text; - let sliders = Row::new() - .width(500) - .spacing(20) - .push( - slider(0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color - }) + let sliders = row![ + slider(0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.b, move |b| { + Message::BackgroundColorChanged(Color { + b, + ..background_color }) - .step(0.01), - ); + }) + .step(0.01), + ] + .width(500) + .spacing(20); - Row::new() - .height(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new().align_items(Alignment::End).push( - Column::new() - .padding(10) - .spacing(10) - .push(Text::new("Background color").style(Color::WHITE)) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push( - text_input("Placeholder", text) - .on_input(Message::TextChanged), - ), - ), - ) - .into() + container( + column![ + text("Background color").color(Color::WHITE), + text(format!("{background_color:?}")) + .size(14) + .color(Color::WHITE), + text_input("Placeholder", &self.input) + .on_input(Message::InputChanged), + sliders, + ] + .spacing(10), + ) + .padding(10) + .height(Length::Fill) + .align_y(alignment::Vertical::Bottom) + .into() } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index ed61459f22..9cd801b278 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -10,11 +10,10 @@ use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; use iced_winit::core::window; -use iced_winit::core::{Color, Font, Pixels, Size}; +use iced_winit::core::{Color, Font, Pixels, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; -use iced_winit::style::Theme; use iced_winit::winit; use iced_winit::Clipboard; @@ -87,7 +86,7 @@ pub fn main() -> Result<(), Box> { }); let surface = instance.create_surface(window.clone())?; - let (format, (device, queue)) = + let (format, adapter, device, queue) = futures::futures::executor::block_on(async { let adapter = wgpu::util::initialize_adapter_from_env_or_default( &instance, @@ -107,6 +106,19 @@ pub fn main() -> Result<(), Box> { let capabilities = surface.get_capabilities(&adapter); + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: adapter_features + & wgpu::Features::default(), + required_limits: needed_limits, + }, + None, + ) + .await + .expect("Request device"); + ( capabilities .formats @@ -115,18 +127,9 @@ pub fn main() -> Result<(), Box> { .find(wgpu::TextureFormat::is_srgb) .or_else(|| capabilities.formats.first().copied()) .expect("Get preferred format"), - adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - required_features: adapter_features - & wgpu::Features::default(), - required_limits: needed_limits, - }, - None, - ) - .await - .expect("Request device"), + adapter, + device, + queue, ) }); @@ -153,7 +156,7 @@ pub fn main() -> Result<(), Box> { // Initialize iced let mut debug = Debug::new(); let mut renderer = Renderer::new( - Backend::new(&device, &queue, Settings::default(), format), + Backend::new(&adapter, &device, &queue, Settings::default(), format), Font::default(), Pixels(16.0), ); @@ -167,7 +170,7 @@ pub fn main() -> Result<(), Box> { // Run event loop event_loop.run(move |event, window_target| { - // You should change this if you want to render continuosly + // You should change this if you want to render continuously window_target.set_control_flow(ControlFlow::Wait); match event { diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index b626c70dd6..713e2b70e0 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,21 +1,22 @@ -use iced::executor; use iced::keyboard; use iced::mouse; -use iced::theme; use iced::widget::{ button, canvas, checkbox, column, container, horizontal_space, pick_list, - row, scrollable, text, vertical_rule, + row, scrollable, text, }; use iced::{ - color, Alignment, Application, Color, Command, Element, Font, Length, - Point, Rectangle, Renderer, Settings, Subscription, Theme, + color, Alignment, Element, Font, Length, Point, Rectangle, Renderer, + Subscription, Theme, }; pub fn main() -> iced::Result { - Layout::run(Settings::default()) + iced::program(Layout::title, Layout::update, Layout::view) + .subscription(Layout::subscription) + .theme(Layout::theme) + .run() } -#[derive(Debug)] +#[derive(Default, Debug)] struct Layout { example: Example, explain: bool, @@ -30,28 +31,12 @@ enum Message { ThemeSelected(Theme), } -impl Application for Layout { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self { - example: Example::default(), - explain: false, - theme: Theme::Light, - }, - Command::none(), - ) - } - +impl Layout { fn title(&self) -> String { format!("{} - Layout - Iced", self.example.title) } - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Message) { match message { Message::Next => { self.example = self.example.next(); @@ -66,8 +51,6 @@ impl Application for Layout { self.theme = theme; } } - - Command::none() } fn subscription(&self) -> Subscription { @@ -85,14 +68,10 @@ impl Application for Layout { fn view(&self) -> Element { let header = row![ text(self.example.title).size(20).font(Font::MONOSPACE), - horizontal_space(Length::Fill), + horizontal_space(), checkbox("Explain", self.explain) .on_toggle(Message::ExplainToggled), - pick_list( - Theme::ALL, - Some(self.theme.clone()), - Message::ThemeSelected - ), + pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected), ] .spacing(20) .align_items(Alignment::Center); @@ -102,7 +81,7 @@ impl Application for Layout { } else { self.example.view() }) - .style(|theme: &Theme| { + .style(|theme, _status| { let palette = theme.extended_palette(); container::Appearance::default() @@ -121,7 +100,7 @@ impl Application for Layout { .on_press(Message::Previous) .into(), ), - Some(horizontal_space(Length::Fill).into()), + Some(horizontal_space().into()), (!self.example.is_last()).then_some( button("Next →") .padding([5, 10]) @@ -171,10 +150,6 @@ impl Example { title: "Application", view: application, }, - Self { - title: "Nested Quotes", - view: nested_quotes, - }, ]; fn is_first(self) -> bool { @@ -255,22 +230,22 @@ fn row_<'a>() -> Element<'a, Message> { } fn space<'a>() -> Element<'a, Message> { - row!["Left!", horizontal_space(Length::Fill), "Right!"].into() + row!["Left!", horizontal_space(), "Right!"].into() } fn application<'a>() -> Element<'a, Message> { let header = container( row![ square(40), - horizontal_space(Length::Fill), + horizontal_space(), "Header!", - horizontal_space(Length::Fill), + horizontal_space(), square(40), ] .padding(10) .align_items(Alignment::Center), ) - .style(|theme: &Theme| { + .style(|theme, _status| { let palette = theme.extended_palette(); container::Appearance::default() @@ -284,7 +259,7 @@ fn application<'a>() -> Element<'a, Message> { .width(200) .align_items(Alignment::Center), ) - .style(theme::Container::Box) + .style(container::rounded_box) .height(Length::Fill) .center_y(); @@ -308,38 +283,6 @@ fn application<'a>() -> Element<'a, Message> { column![header, row![sidebar, content]].into() } -fn nested_quotes<'a>() -> Element<'a, Message> { - (1..5) - .fold(column![text("Original text")].padding(10), |quotes, i| { - column![ - container( - row![vertical_rule(2), quotes].height(Length::Shrink) - ) - .style(|theme: &Theme| { - let palette = theme.extended_palette(); - - container::Appearance::default().with_background( - if palette.is_dark { - Color { - a: 0.01, - ..Color::WHITE - } - } else { - Color { - a: 0.08, - ..Color::BLACK - } - }, - ) - }), - text(format!("Reply {i}")) - ] - .spacing(10) - .padding(10) - }) - .into() -} - fn square<'a>(size: impl Into + Copy) -> Element<'a, Message> { struct Square; diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 04df0744f9..2d53df9335 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,15 +1,14 @@ -use iced::theme; use iced::widget::{ button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, }; -use iced::{Element, Length, Sandbox, Settings}; +use iced::{Element, Length}; use std::collections::HashSet; use std::hash::Hash; pub fn main() -> iced::Result { - App::run(Settings::default()) + iced::run("Lazy - Iced", App::update, App::view) } struct App { @@ -121,17 +120,7 @@ enum Message { ItemColorChanged(Item, Color), } -impl Sandbox for App { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Lazy - Iced") - } - +impl App { fn update(&mut self, message: Message) { match message { Message::InputChanged(input) => { @@ -181,12 +170,11 @@ impl Sandbox for App { column(items.into_iter().map(|item| { let button = button("Delete") .on_press(Message::DeleteItem(item.clone())) - .style(theme::Button::Destructive); + .style(button::danger); row![ - text(&item.name) - .style(theme::Text::Color(item.color.into())), - horizontal_space(Length::Fill), + text(&item.name).color(item.color), + horizontal_space(), pick_list(Color::ALL, Some(item.color), move |color| { Message::ItemColorChanged(item.clone(), color) }), diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 93a4605ee6..eaa4d57ed4 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -1,6 +1,5 @@ -use iced::executor; use iced::widget::{column, container, row, slider, text}; -use iced::{Application, Command, Element, Length, Settings, Theme}; +use iced::{Element, Length}; use std::time::Duration; @@ -12,51 +11,31 @@ use circular::Circular; use linear::Linear; pub fn main() -> iced::Result { - LoadingSpinners::run(Settings { - antialiasing: true, - ..Default::default() - }) + iced::program( + "Loading Spinners - Iced", + LoadingSpinners::update, + LoadingSpinners::view, + ) + .antialiasing(true) + .run() } struct LoadingSpinners { cycle_duration: f32, } -impl Default for LoadingSpinners { - fn default() -> Self { - Self { - cycle_duration: 2.0, - } - } -} - #[derive(Debug, Clone, Copy)] enum Message { CycleDurationChanged(f32), } -impl Application for LoadingSpinners { - type Message = Message; - type Flags = (); - type Executor = executor::Default; - type Theme = Theme; - - fn new(_flags: Self::Flags) -> (Self, Command) { - (Self::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Loading Spinners - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl LoadingSpinners { + fn update(&mut self, message: Message) { match message { Message::CycleDurationChanged(duration) => { self.cycle_duration = duration; } } - - Command::none() } fn view(&self) -> Element { @@ -115,3 +94,11 @@ impl Application for LoadingSpinners { .into() } } + +impl Default for LoadingSpinners { + fn default() -> Self { + Self { + cycle_duration: 2.0, + } + } +} diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 8602edb7df..6a5ff123ab 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -1,39 +1,30 @@ use iced::widget::{button, column, container, text}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Alignment, Element, Length}; use loupe::loupe; pub fn main() -> iced::Result { - Counter::run(Settings::default()) + iced::run("Loupe - Iced", Loupe::update, Loupe::view) } -struct Counter { - value: i32, +#[derive(Default)] +struct Loupe { + value: i64, } #[derive(Debug, Clone, Copy)] enum Message { - IncrementPressed, - DecrementPressed, + Increment, + Decrement, } -impl Sandbox for Counter { - type Message = Message; - - fn new() -> Self { - Self { value: 0 } - } - - fn title(&self) -> String { - String::from("Counter - Iced") - } - +impl Loupe { fn update(&mut self, message: Message) { match message { - Message::IncrementPressed => { + Message::Increment => { self.value += 1; } - Message::DecrementPressed => { + Message::Decrement => { self.value -= 1; } } @@ -43,9 +34,9 @@ impl Sandbox for Counter { container(loupe( 3.0, column![ - button("Increment").on_press(Message::IncrementPressed), + button("Increment").on_press(Message::Increment), text(self.value).size(50), - button("Decrement").on_press(Message::DecrementPressed) + button("Decrement").on_press(Message::Decrement) ] .padding(20) .align_items(Alignment::Center), diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 6fe951ee51..398728e0a5 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -1,21 +1,19 @@ use iced::event::{self, Event}; -use iced::executor; use iced::keyboard; use iced::keyboard::key; -use iced::theme; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, text_input, }; -use iced::{ - Alignment, Application, Command, Element, Length, Settings, Subscription, -}; +use iced::{Alignment, Command, Element, Length, Subscription}; use modal::Modal; use std::fmt; pub fn main() -> iced::Result { - App::run(Settings::default()) + iced::program("Modal - Iced", App::update, App::view) + .subscription(App::subscription) + .run() } #[derive(Default)] @@ -37,21 +35,8 @@ enum Message { Event(Event), } -impl Application for App { - type Executor = executor::Default; - type Message = Message; - type Theme = iced::Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - (App::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Modal - Iced") - } - - fn subscription(&self) -> Subscription { +impl App { + fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } @@ -111,13 +96,9 @@ impl Application for App { fn view(&self) -> Element { let content = container( column![ - row![ - text("Top Left"), - horizontal_space(Length::Fill), - text("Top Right") - ] - .align_items(Alignment::Start) - .height(Length::Fill), + row![text("Top Left"), horizontal_space(), text("Top Right")] + .align_items(Alignment::Start) + .height(Length::Fill), container( button(text("Show Modal")).on_press(Message::ShowModal) ) @@ -127,7 +108,7 @@ impl Application for App { .height(Length::Fill), row![ text("Bottom Left"), - horizontal_space(Length::Fill), + horizontal_space(), text("Bottom Right") ] .align_items(Alignment::End) @@ -157,7 +138,7 @@ impl Application for App { text_input("", &self.password) .on_input(Message::Password) .on_submit(Message::Submit) - .password() + .secure(true) .padding(5), ] .spacing(5), @@ -179,7 +160,7 @@ impl Application for App { ) .width(300) .padding(10) - .style(theme::Container::Box); + .style(container::rounded_box); Modal::new(content, modal) .on_blur(Message::HideModal) diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 956ad4710e..2453c7f5c2 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -2,101 +2,58 @@ //! a circle around each fingertip. This only works on touch-enabled //! computers like Microsoft Surface. use iced::mouse; +use iced::touch; use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{self, Canvas, Geometry}; -use iced::{ - executor, touch, window, Application, Color, Command, Element, Length, - Point, Rectangle, Renderer, Settings, Subscription, Theme, -}; +use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme}; use std::collections::HashMap; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - Multitouch::run(Settings { - antialiasing: true, - window: window::Settings { - position: window::Position::Centered, - ..window::Settings::default() - }, - ..Settings::default() - }) + iced::program("Multitouch - Iced", Multitouch::update, Multitouch::view) + .antialiasing(true) + .centered() + .run() } +#[derive(Default)] struct Multitouch { - state: State, -} - -#[derive(Debug)] -struct State { cache: canvas::Cache, fingers: HashMap, } -impl State { - fn new() -> Self { - Self { - cache: canvas::Cache::new(), - fingers: HashMap::new(), - } - } -} - #[derive(Debug)] enum Message { FingerPressed { id: touch::Finger, position: Point }, FingerLifted { id: touch::Finger }, } -impl Application for Multitouch { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Multitouch { - state: State::new(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Multitouch - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl Multitouch { + fn update(&mut self, message: Message) { match message { Message::FingerPressed { id, position } => { - self.state.fingers.insert(id, position); - self.state.cache.clear(); + self.fingers.insert(id, position); + self.cache.clear(); } Message::FingerLifted { id } => { - self.state.fingers.remove(&id); - self.state.cache.clear(); + self.fingers.remove(&id); + self.cache.clear(); } } - - Command::none() - } - - fn subscription(&self) -> Subscription { - Subscription::none() } fn view(&self) -> Element { - Canvas::new(&self.state) + Canvas::new(self) .width(Length::Fill) .height(Length::Fill) .into() } } -impl canvas::Program for State { +impl canvas::Program for Multitouch { type State = (); fn update( diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 742dc344c7..829996d87e 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,17 +1,15 @@ use iced::alignment::{self, Alignment}; -use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ button, column, container, responsive, row, scrollable, text, }; -use iced::{ - Application, Color, Command, Element, Length, Settings, Size, Subscription, -}; +use iced::{Color, Element, Length, Size, Subscription}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::program("Pane Grid - Iced", Example::update, Example::view) + .subscription(Example::subscription) + .run() } struct Example { @@ -35,30 +33,18 @@ enum Message { CloseFocused, } -impl Application for Example { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { +impl Example { + fn new() -> Self { let (panes, _) = pane_grid::State::new(Pane::new(0)); - ( - Example { - panes, - panes_created: 1, - focus: None, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Pane grid - Iced") + Example { + panes, + panes_created: 1, + focus: None, + } } - fn update(&mut self, message: Message) -> Command { + fn update(&mut self, message: Message) { match message { Message::Split(axis, pane) => { let result = @@ -132,8 +118,6 @@ impl Application for Example { } } } - - Command::none() } fn subscription(&self) -> Subscription { @@ -162,7 +146,7 @@ impl Application for Example { let title = row![ pin_button, "Pane", - text(pane.id.to_string()).style(if is_focused { + text(pane.id.to_string()).color(if is_focused { PANE_ID_COLOR_FOCUSED } else { PANE_ID_COLOR_UNFOCUSED @@ -209,6 +193,12 @@ impl Application for Example { } } +impl Default for Example { + fn default() -> Self { + Example::new() + } +} + const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( 0xFF as f32 / 255.0, 0xC7 as f32 / 255.0, @@ -276,7 +266,7 @@ fn view_content<'a>( .on_press(message) }; - let mut controls = column![ + let controls = column![ button( "Split horizontally", Message::Split(pane_grid::Axis::Horizontal, pane), @@ -286,16 +276,14 @@ fn view_content<'a>( Message::Split(pane_grid::Axis::Vertical, pane), ) ] + .push_maybe(if total_panes > 1 && !is_pinned { + Some(button("Close", Message::Close(pane)).style(button::danger)) + } else { + None + }) .spacing(5) .max_width(160); - if total_panes > 1 && !is_pinned { - controls = controls.push( - button("Close", Message::Close(pane)) - .style(theme::Button::Destructive), - ); - } - let content = column![ text(format!("{}x{}", size.width, size.height)).size(24), controls, @@ -317,31 +305,31 @@ fn view_controls<'a>( is_pinned: bool, is_maximized: bool, ) -> Element<'a, Message> { - let mut row = row![].spacing(5); + let row = row![].spacing(5).push_maybe(if total_panes > 1 { + let (content, message) = if is_maximized { + ("Restore", Message::Restore) + } else { + ("Maximize", Message::Maximize(pane)) + }; - if total_panes > 1 { - let toggle = { - let (content, message) = if is_maximized { - ("Restore", Message::Restore) - } else { - ("Maximize", Message::Maximize(pane)) - }; + Some( button(text(content).size(14)) - .style(theme::Button::Secondary) + .style(button::secondary) .padding(3) - .on_press(message) - }; - - row = row.push(toggle); - } - - let mut close = button(text("Close").size(14)) - .style(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - close = close.on_press(Message::Close(pane)); - } + .on_press(message), + ) + } else { + None + }); + + let close = button(text("Close").size(14)) + .style(button::danger) + .padding(3) + .on_press_maybe(if total_panes > 1 && !is_pinned { + Some(Message::Close(pane)) + } else { + None + }); row.push(close).into() } @@ -350,7 +338,10 @@ mod style { use iced::widget::container; use iced::{Border, Theme}; - pub fn title_bar_active(theme: &Theme) -> container::Appearance { + pub fn title_bar_active( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { let palette = theme.extended_palette(); container::Appearance { @@ -360,7 +351,10 @@ mod style { } } - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + pub fn title_bar_focused( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { let palette = theme.extended_palette(); container::Appearance { @@ -370,7 +364,10 @@ mod style { } } - pub fn pane_active(theme: &Theme) -> container::Appearance { + pub fn pane_active( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { let palette = theme.extended_palette(); container::Appearance { @@ -384,7 +381,10 @@ mod style { } } - pub fn pane_focused(theme: &Theme) -> container::Appearance { + pub fn pane_focused( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { let palette = theme.extended_palette(); container::Appearance { diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index e4d96dc8e9..2be6f5b0a3 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,8 +1,8 @@ use iced::widget::{column, pick_list, scrollable, vertical_space}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::{Alignment, Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Pick List - Iced", Example::update, Example::view) } #[derive(Default)] @@ -15,17 +15,7 @@ enum Message { LanguageSelected(Language), } -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Pick list - Iced") - } - +impl Example { fn update(&mut self, message: Message) { match message { Message::LanguageSelected(language) => { @@ -43,10 +33,10 @@ impl Sandbox for Example { .placeholder("Choose a language..."); let content = column![ - vertical_space(600), + vertical_space().height(600), "Which is your favorite language?", pick_list, - vertical_space(600), + vertical_space().height(600), ] .width(Length::Fill) .align_items(Alignment::Center) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8b71a26950..0811c08d99 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,17 +1,20 @@ use iced::futures; use iced::widget::{self, column, container, image, row, text}; -use iced::{ - Alignment, Application, Color, Command, Element, Length, Settings, Theme, -}; +use iced::{Alignment, Command, Element, Length}; pub fn main() -> iced::Result { - Pokedex::run(Settings::default()) + iced::program(Pokedex::title, Pokedex::update, Pokedex::view) + .load(Pokedex::search) + .run() } -#[derive(Debug)] +#[derive(Debug, Default)] enum Pokedex { + #[default] Loading, - Loaded { pokemon: Pokemon }, + Loaded { + pokemon: Pokemon, + }, Errored, } @@ -21,17 +24,9 @@ enum Message { Search, } -impl Application for Pokedex { - type Message = Message; - type Theme = Theme; - type Executor = iced::executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Pokedex, Command) { - ( - Pokedex::Loading, - Command::perform(Pokemon::search(), Message::PokemonFound), - ) +impl Pokedex { + fn search() -> Command { + Command::perform(Pokemon::search(), Message::PokemonFound) } fn title(&self) -> String { @@ -61,7 +56,7 @@ impl Application for Pokedex { _ => { *self = Pokedex::Loading; - Command::perform(Pokemon::search(), Message::PokemonFound) + Self::search() } }, } @@ -116,7 +111,7 @@ impl Pokemon { text(&self.name).size(30).width(Length::Fill), text(format!("#{}", self.number)) .size(20) - .style(Color::from([0.5, 0.5, 0.5])), + .color([0.5, 0.5, 0.5]), ] .align_items(Alignment::Center) .spacing(20), diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index d4ebe4d31e..67da62f2cd 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -1,8 +1,8 @@ use iced::widget::{column, progress_bar, slider}; -use iced::{Element, Sandbox, Settings}; +use iced::Element; pub fn main() -> iced::Result { - Progress::run(Settings::default()) + iced::run("Progress Bar - Iced", Progress::update, Progress::view) } #[derive(Default)] @@ -15,17 +15,7 @@ enum Message { SliderChanged(f32), } -impl Sandbox for Progress { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("A simple Progressbar") - } - +impl Progress { fn update(&mut self, message: Message) { match message { Message::SliderChanged(x) => self.value = x, diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 8b2e950020..b93adf0436 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,9 +1,16 @@ -use iced::widget::qr_code::{self, QRCode}; -use iced::widget::{column, container, pick_list, row, text, text_input}; -use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; +use iced::widget::{ + column, container, pick_list, qr_code, row, text, text_input, +}; +use iced::{Alignment, Element, Length, Theme}; pub fn main() -> iced::Result { - QRGenerator::run(Settings::default()) + iced::program( + "QR Code Generator - Iced", + QRGenerator::update, + QRGenerator::view, + ) + .theme(QRGenerator::theme) + .run() } #[derive(Default)] @@ -19,17 +26,7 @@ enum Message { ThemeChanged(Theme), } -impl Sandbox for QRGenerator { - type Message = Message; - - fn new() -> Self { - QRGenerator::default() - } - - fn title(&self) -> String { - String::from("QR Code Generator - Iced") - } - +impl QRGenerator { fn update(&mut self, message: Message) { match message { Message::DataChanged(mut data) => { @@ -60,24 +57,21 @@ impl Sandbox for QRGenerator { let choose_theme = row![ text("Theme:"), - pick_list( - Theme::ALL, - Some(self.theme.clone()), - Message::ThemeChanged, - ) + pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) ] .spacing(10) .align_items(Alignment::Center); - let mut content = column![title, input, choose_theme] + let content = column![title, input, choose_theme] + .push_maybe( + self.qr_code + .as_ref() + .map(|data| qr_code(data).cell_size(10)), + ) .width(700) .spacing(20) .align_items(Alignment::Center); - if let Some(qr_code) = self.qr_code.as_ref() { - content = content.push(QRCode::new(qr_code).cell_size(10)); - } - container(content) .width(Length::Fill) .height(Length::Fill) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 64aa1a7a8c..d887c41b50 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,13 +1,10 @@ use iced::alignment; -use iced::executor; use iced::keyboard; -use iced::theme; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; use iced::{ - Alignment, Application, Command, ContentFit, Element, Length, Rectangle, - Subscription, Theme, + Alignment, Command, ContentFit, Element, Length, Rectangle, Subscription, }; use ::image as img; @@ -16,9 +13,12 @@ use ::image::ColorType; fn main() -> iced::Result { tracing_subscriber::fmt::init(); - Example::run(iced::Settings::default()) + iced::program("Screenshot - Iced", Example::update, Example::view) + .subscription(Example::subscription) + .run() } +#[derive(Default)] struct Example { screenshot: Option, saved_png_path: Option>, @@ -43,33 +43,8 @@ enum Message { HeightInputChanged(Option), } -impl Application for Example { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Example { - screenshot: None, - saved_png_path: None, - png_saving: false, - crop_error: None, - x_input_value: None, - y_input_value: None, - width_input_value: None, - height_input_value: None, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - "Screenshot".to_string() - } - - fn update(&mut self, message: Self::Message) -> Command { +impl Example { + fn update(&mut self, message: Message) -> Command { match message { Message::Screenshot => { return iced::window::screenshot( @@ -131,7 +106,7 @@ impl Application for Example { Command::none() } - fn view(&self) -> Element<'_, Self::Message> { + fn view(&self) -> Element<'_, Message> { let image: Element = if let Some(screenshot) = &self.screenshot { image(image::Handle::from_pixels( @@ -149,7 +124,7 @@ impl Application for Example { let image = container(image) .padding(10) - .style(theme::Container::Box) + .style(container::rounded_box) .width(Length::FillPortion(2)) .height(Length::Fill) .center_x() @@ -183,58 +158,60 @@ impl Application for Example { .spacing(10) .align_items(Alignment::Center); - let mut crop_controls = + let crop_controls = column![crop_origin_controls, crop_dimension_controls] + .push_maybe( + self.crop_error + .as_ref() + .map(|error| text(format!("Crop error! \n{error}"))), + ) .spacing(10) .align_items(Alignment::Center); - if let Some(crop_error) = &self.crop_error { - crop_controls = - crop_controls.push(text(format!("Crop error! \n{crop_error}"))); - } + let controls = { + let save_result = + self.saved_png_path.as_ref().map( + |png_result| match png_result { + Ok(path) => format!("Png saved as: {path:?}!"), + Err(PngError(error)) => { + format!("Png could not be saved due to:\n{}", error) + } + }, + ); - let mut controls = column![ column![ - button(centered_text("Screenshot!")) + column![ + button(centered_text("Screenshot!")) + .padding([10, 20, 10, 20]) + .width(Length::Fill) + .on_press(Message::Screenshot), + if !self.png_saving { + button(centered_text("Save as png")).on_press_maybe( + self.screenshot.is_some().then(|| Message::Png), + ) + } else { + button(centered_text("Saving...")) + .style(button::secondary) + } + .style(button::secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) - .on_press(Message::Screenshot), - if !self.png_saving { - button(centered_text("Save as png")).on_press_maybe( - self.screenshot.is_some().then(|| Message::Png), - ) - } else { - button(centered_text("Saving...")) - .style(theme::Button::Secondary) - } - .style(theme::Button::Secondary) - .padding([10, 20, 10, 20]) - .width(Length::Fill) - ] - .spacing(10), - column![ - crop_controls, - button(centered_text("Crop")) - .on_press(Message::Crop) - .style(theme::Button::Destructive) - .padding([10, 20, 10, 20]) - .width(Length::Fill), + ] + .spacing(10), + column![ + crop_controls, + button(centered_text("Crop")) + .on_press(Message::Crop) + .style(button::danger) + .padding([10, 20, 10, 20]) + .width(Length::Fill), + ] + .spacing(10) + .align_items(Alignment::Center), ] - .spacing(10) - .align_items(Alignment::Center), - ] - .spacing(40); - - if let Some(png_result) = &self.saved_png_path { - let msg = match png_result { - Ok(path) => format!("Png saved as: {path:?}!"), - Err(PngError(error)) => { - format!("Png could not be saved due to:\n{}", error) - } - }; - - controls = controls.push(text(msg)); - } + .push_maybe(save_result.map(text)) + .spacing(40) + }; let side_content = container(controls) .align_x(alignment::Horizontal::Center) @@ -258,7 +235,7 @@ impl Application for Example { .into() } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { use keyboard::key; keyboard::on_key_press(|key, _modifiers| { diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index ff69191724..240ae908c1 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,21 +1,22 @@ -use iced::executor; -use iced::theme; -use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; +use iced::widget::scrollable::Properties; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, - scrollable, slider, text, vertical_space, -}; -use iced::{ - Alignment, Application, Border, Color, Command, Element, Length, Settings, - Theme, + scrollable, slider, text, vertical_space, Scrollable, }; +use iced::{Alignment, Border, Color, Command, Element, Length, Theme}; use once_cell::sync::Lazy; static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); pub fn main() -> iced::Result { - ScrollableDemo::run(Settings::default()) + iced::program( + "Scrollable - Iced", + ScrollableDemo::update, + ScrollableDemo::view, + ) + .theme(ScrollableDemo::theme) + .run() } struct ScrollableDemo { @@ -46,28 +47,16 @@ enum Message { Scrolled(scrollable::Viewport), } -impl Application for ScrollableDemo { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - ScrollableDemo { - scrollable_direction: Direction::Vertical, - scrollbar_width: 10, - scrollbar_margin: 0, - scroller_width: 10, - current_scroll_offset: scrollable::RelativeOffset::START, - alignment: scrollable::Alignment::Start, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Scrollable - Iced") +impl ScrollableDemo { + fn new() -> Self { + ScrollableDemo { + scrollable_direction: Direction::Vertical, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, + current_scroll_offset: scrollable::RelativeOffset::START, + alignment: scrollable::Alignment::Start, + } } fn update(&mut self, message: Message) -> Command { @@ -214,38 +203,38 @@ impl Application for ScrollableDemo { let scrollable_content: Element = Element::from(match self.scrollable_direction { - Direction::Vertical => scrollable( + Direction::Vertical => Scrollable::with_direction( column![ scroll_to_end_button(), text("Beginning!"), - vertical_space(1200), + vertical_space().height(1200), text("Middle!"), - vertical_space(1200), + vertical_space().height(1200), text("End!"), scroll_to_beginning_button(), ] .align_items(Alignment::Center) .padding([40, 0, 40, 0]) .spacing(40), + scrollable::Direction::Vertical( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment), + ), ) .width(Length::Fill) .height(Length::Fill) - .direction(scrollable::Direction::Vertical( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - )) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Horizontal => scrollable( + Direction::Horizontal => Scrollable::with_direction( row![ scroll_to_end_button(), text("Beginning!"), - horizontal_space(1200), + horizontal_space().width(1200), text("Middle!"), - horizontal_space(1200), + horizontal_space().width(1200), text("End!"), scroll_to_beginning_button(), ] @@ -253,67 +242,63 @@ impl Application for ScrollableDemo { .align_items(Alignment::Center) .padding([0, 40, 0, 40]) .spacing(40), + scrollable::Direction::Horizontal( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment), + ), ) .width(Length::Fill) .height(Length::Fill) - .direction(scrollable::Direction::Horizontal( - Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment), - )) - .style(theme::Scrollable::custom(ScrollbarCustomStyle)) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), - Direction::Multi => scrollable( + Direction::Multi => Scrollable::with_direction( //horizontal content row![ column![ text("Let's do some scrolling!"), - vertical_space(2400) + vertical_space().height(2400) ], scroll_to_end_button(), text("Horizontal - Beginning!"), - horizontal_space(1200), + horizontal_space().width(1200), //vertical content column![ text("Horizontal - Middle!"), scroll_to_end_button(), text("Vertical - Beginning!"), - vertical_space(1200), + vertical_space().height(1200), text("Vertical - Middle!"), - vertical_space(1200), + vertical_space().height(1200), text("Vertical - End!"), scroll_to_beginning_button(), - vertical_space(40), + vertical_space().height(40), ] .spacing(40), - horizontal_space(1200), + horizontal_space().width(1200), text("Horizontal - End!"), scroll_to_beginning_button(), ] .align_items(Alignment::Center) .padding([0, 40, 0, 40]) .spacing(40), + { + let properties = Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width) + .alignment(self.alignment); + + scrollable::Direction::Both { + horizontal: properties, + vertical: properties, + } + }, ) .width(Length::Fill) .height(Length::Fill) - .direction({ - let properties = Properties::new() - .width(self.scrollbar_width) - .margin(self.scrollbar_margin) - .scroller_width(self.scroller_width) - .alignment(self.alignment); - - scrollable::Direction::Both { - horizontal: properties, - vertical: properties, - } - }) - .style(theme::Scrollable::Custom(Box::new( - ScrollbarCustomStyle, - ))) .id(SCROLLABLE_ID.clone()) .on_scroll(Message::Scrolled), }); @@ -345,47 +330,14 @@ impl Application for ScrollableDemo { container(content).padding(20).center_x().center_y().into() } - fn theme(&self) -> Self::Theme { + fn theme(&self) -> Theme { Theme::Dark } } -struct ScrollbarCustomStyle; - -impl scrollable::StyleSheet for ScrollbarCustomStyle { - type Style = Theme; - - fn active(&self, style: &Self::Style) -> Scrollbar { - style.active(&theme::Scrollable::Default) - } - - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> Scrollbar { - style.hovered(&theme::Scrollable::Default, is_mouse_over_scrollbar) - } - - fn hovered_horizontal( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> Scrollbar { - if is_mouse_over_scrollbar { - Scrollbar { - background: style - .active(&theme::Scrollable::default()) - .background, - border: Border::with_radius(2), - scroller: Scroller { - color: Color::from_rgb8(250, 85, 134), - border: Border::with_radius(2), - }, - } - } else { - self.active(style) - } +impl Default for ScrollableDemo { + fn default() -> Self { + Self::new() } } @@ -393,6 +345,6 @@ fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance { progress_bar::Appearance { background: theme.extended_palette().background.strong.color.into(), bar: Color::from_rgb8(250, 85, 134).into(), - border_radius: 0.0.into(), + border: Border::default(), } } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 01a114bbd2..07ae05d6ae 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,25 +1,23 @@ -use std::fmt::Debug; - -use iced::executor; use iced::mouse; 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, Renderer, Settings, - Size, Theme, -}; +use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme}; use rand::Rng; +use std::fmt::Debug; fn main() -> iced::Result { - SierpinskiEmulator::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program( + "Sierpinski Triangle - Iced", + SierpinskiEmulator::update, + SierpinskiEmulator::view, + ) + .antialiasing(true) + .run() } -#[derive(Debug)] +#[derive(Debug, Default)] struct SierpinskiEmulator { graph: SierpinskiGraph, } @@ -31,27 +29,8 @@ pub enum Message { 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) { - 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 { +impl SierpinskiEmulator { + fn update(&mut self, message: Message) { match message { Message::IterationSet(cur_iter) => { self.graph.iteration = cur_iter; @@ -67,11 +46,9 @@ impl Application for SierpinskiEmulator { } self.graph.redraw(); - - Command::none() } - fn view(&self) -> iced::Element<'_, Self::Message> { + fn view(&self) -> iced::Element<'_, Message> { column![ Canvas::new(&self.graph) .width(Length::Fill) @@ -167,10 +144,6 @@ impl canvas::Program for SierpinskiGraph { } impl SierpinskiGraph { - fn new() -> SierpinskiGraph { - SierpinskiGraph::default() - } - fn redraw(&mut self) { self.cache.clear(); } diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index f71dac01fa..b3a47614c8 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -1,8 +1,8 @@ use iced::widget::{column, container, slider, text, vertical_slider}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::{Element, Length}; pub fn main() -> iced::Result { - Slider::run(Settings::default()) + iced::run("Slider - Iced", Slider::update, Slider::view) } #[derive(Debug, Clone)] @@ -17,10 +17,8 @@ pub struct Slider { shift_step: u8, } -impl Sandbox for Slider { - type Message = Message; - - fn new() -> Slider { +impl Slider { + fn new() -> Self { Slider { value: 50, default: 50, @@ -29,10 +27,6 @@ impl Sandbox for Slider { } } - fn title(&self) -> String { - String::from("Slider - Iced") - } - fn update(&mut self, message: Message) { match message { Message::SliderChanged(value) => { @@ -75,3 +69,9 @@ impl Sandbox for Slider { .into() } } + +impl Default for Slider { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index a58ca683df..b5228f0957 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -6,18 +6,15 @@ //! 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::mouse; -use iced::theme::{self, Theme}; use iced::widget::canvas; use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::Path; use iced::window; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Renderer, - Settings, Size, Subscription, Vector, + Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription, + Theme, Vector, }; use std::time::Instant; @@ -25,12 +22,17 @@ use std::time::Instant; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - SolarSystem::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program( + "Solar System - Iced", + SolarSystem::update, + SolarSystem::view, + ) + .subscription(SolarSystem::subscription) + .theme(SolarSystem::theme) + .run() } +#[derive(Default)] struct SolarSystem { state: State, } @@ -40,33 +42,13 @@ enum Message { Tick(Instant), } -impl Application for SolarSystem { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - SolarSystem { - state: State::new(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Solar system - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl SolarSystem { + fn update(&mut self, message: Message) { match message { Message::Tick(instant) => { self.state.update(instant); } } - - Command::none() } fn view(&self) -> Element { @@ -77,18 +59,7 @@ impl Application for SolarSystem { } fn theme(&self) -> Theme { - Theme::Dark - } - - fn style(&self) -> theme::Application { - fn dark_background(_theme: &Theme) -> application::Appearance { - application::Appearance { - background_color: Color::BLACK, - text_color: Color::WHITE, - } - } - - theme::Application::custom(dark_background) + Theme::Moonfly } fn subscription(&self) -> Subscription { @@ -229,3 +200,9 @@ impl canvas::Program for State { vec![background, system] } } + +impl Default for State { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 8a0674c136..b9eb19cfb8 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,27 +1,31 @@ use iced::alignment; -use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::time; use iced::widget::{button, column, container, row, text}; -use iced::{ - Alignment, Application, Command, Element, Length, Settings, Subscription, -}; +use iced::{Alignment, Element, Length, Subscription, Theme}; use std::time::{Duration, Instant}; pub fn main() -> iced::Result { - Stopwatch::run(Settings::default()) + iced::program("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) + .subscription(Stopwatch::subscription) + .theme(Stopwatch::theme) + .run() } +#[derive(Default)] struct Stopwatch { duration: Duration, state: State, } +#[derive(Default)] enum State { + #[default] Idle, - Ticking { last_tick: Instant }, + Ticking { + last_tick: Instant, + }, } #[derive(Debug, Clone)] @@ -31,27 +35,8 @@ enum Message { Tick(Instant), } -impl Application for Stopwatch { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Stopwatch, Command) { - ( - Stopwatch { - duration: Duration::default(), - state: State::Idle, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Stopwatch - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl Stopwatch { + fn update(&mut self, message: Message) { match message { Message::Toggle => match self.state { State::Idle => { @@ -73,8 +58,6 @@ impl Application for Stopwatch { self.duration = Duration::default(); } } - - Command::none() } fn subscription(&self) -> Subscription { @@ -136,7 +119,7 @@ impl Application for Stopwatch { }; let reset_button = button("Reset") - .style(theme::Button::Destructive) + .style(button::danger) .on_press(Message::Reset); let controls = row![toggle_button, reset_button].spacing(20); diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index cf2dcb8aaf..73268da01d 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -3,10 +3,12 @@ use iced::widget::{ progress_bar, row, scrollable, slider, text, text_input, toggler, vertical_rule, vertical_space, }; -use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; +use iced::{Alignment, Element, Length, Theme}; pub fn main() -> iced::Result { - Styling::run(Settings::default()) + iced::program("Styling - Iced", Styling::update, Styling::view) + .theme(Styling::theme) + .run() } #[derive(Default)] @@ -28,17 +30,7 @@ enum Message { TogglerToggled(bool), } -impl Sandbox for Styling { - type Message = Message; - - fn new() -> Self { - Styling::default() - } - - fn title(&self) -> String { - String::from("Styling - Iced") - } - +impl Styling { fn update(&mut self, message: Message) { match message { Message::ThemeChanged(theme) => { @@ -55,12 +47,8 @@ impl Sandbox for Styling { fn view(&self) -> Element { let choose_theme = column![ text("Theme:"), - pick_list( - Theme::ALL, - Some(self.theme.clone()), - Message::ThemeChanged - ) - .width(Length::Fill), + pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged) + .width(Length::Fill), ] .spacing(10); @@ -80,7 +68,7 @@ impl Sandbox for Styling { let scrollable = scrollable(column![ "Scroll me!", - vertical_space(800), + vertical_space().height(800), "You did it!" ]) .width(Length::Fill) diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index ba93007ca8..cc686dcad8 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,9 +1,8 @@ -use iced::theme; use iced::widget::{checkbox, column, container, svg}; -use iced::{color, Element, Length, Sandbox, Settings}; +use iced::{color, Element, Length}; pub fn main() -> iced::Result { - Tiger::run(Settings::default()) + iced::run("SVG - Iced", Tiger::update, Tiger::view) } #[derive(Debug, Default)] @@ -16,18 +15,8 @@ pub enum Message { ToggleColorFilter(bool), } -impl Sandbox for Tiger { - type Message = Message; - - fn new() -> Self { - Tiger::default() - } - - fn title(&self) -> String { - String::from("SVG - Iced") - } - - fn update(&mut self, message: Self::Message) { +impl Tiger { + fn update(&mut self, message: Message) { match message { Message::ToggleColorFilter(apply_color_filter) => { self.apply_color_filter = apply_color_filter; @@ -35,19 +24,19 @@ impl Sandbox for Tiger { } } - fn view(&self) -> Element { + fn view(&self) -> Element { let handle = svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") )); let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( - if self.apply_color_filter { - theme::Svg::custom_fn(|_theme| svg::Appearance { - color: Some(color!(0x0000ff)), - }) - } else { - theme::Svg::Default + |_theme, _status| svg::Appearance { + color: if self.apply_color_filter { + Some(color!(0x0000ff)) + } else { + None + }, }, ); diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 31dc92f165..a6ac27a694 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -1,18 +1,19 @@ use iced::widget::{button, column, container, text}; -use iced::{ - executor, system, Application, Command, Element, Length, Settings, Theme, -}; - -use bytesize::ByteSize; +use iced::{system, Command, Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::program("System Information - Iced", Example::update, Example::view) + .run() } +#[derive(Default)] #[allow(clippy::large_enum_variant)] enum Example { + #[default] Loading, - Loaded { information: system::Information }, + Loaded { + information: system::Information, + }, } #[derive(Clone, Debug)] @@ -22,23 +23,7 @@ enum Message { Refresh, } -impl Application for Example { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - Self::Loading, - system::fetch_information(Message::InformationReceived), - ) - } - - fn title(&self) -> String { - String::from("System Information - Iced") - } - +impl Example { fn update(&mut self, message: Message) -> Command { match message { Message::Refresh => { @@ -55,6 +40,8 @@ impl Application for Example { } fn view(&self) -> Element { + use bytesize::ByteSize; + let content: Element<_> = match self { Example::Loading => text("Loading...").size(40).into(), Example::Loaded { information } => { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 7f067e2fae..fdae1dc177 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -1,21 +1,19 @@ use iced::event::{self, Event}; -use iced::executor; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ self, button, column, container, pick_list, row, slider, text, text_input, }; -use iced::{ - Alignment, Application, Command, Element, Length, Settings, Subscription, -}; +use iced::{Alignment, Command, Element, Length, Subscription}; use toast::{Status, Toast}; pub fn main() -> iced::Result { - App::run(Settings::default()) + iced::program("Toast - Iced", App::update, App::view) + .subscription(App::subscription) + .run() } -#[derive(Default)] struct App { toasts: Vec, editing: Toast, @@ -34,32 +32,20 @@ enum Message { Event(Event), } -impl Application for App { - type Executor = executor::Default; - type Message = Message; - type Theme = iced::Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - ( - App { - toasts: vec![Toast { - title: "Example Toast".into(), - body: "Add more toasts in the form below!".into(), - status: Status::Primary, - }], - timeout_secs: toast::DEFAULT_TIMEOUT, - ..Default::default() - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Toast - Iced") +impl App { + fn new() -> Self { + App { + toasts: vec![Toast { + title: "Example Toast".into(), + body: "Add more toasts in the form below!".into(), + status: Status::Primary, + }], + timeout_secs: toast::DEFAULT_TIMEOUT, + editing: Toast::default(), + } } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } @@ -106,16 +92,15 @@ impl Application for App { } } - fn view<'a>(&'a self) -> Element<'a, Message> { - let subtitle = |title, content: Element<'a, Message>| { + fn view(&self) -> Element<'_, Message> { + let subtitle = |title, content: Element<'static, Message>| { column![text(title).size(14), content].spacing(5) }; - let mut add_toast = button("Add Toast"); - - if !self.editing.body.is_empty() && !self.editing.title.is_empty() { - add_toast = add_toast.on_press(Message::Add); - } + let add_toast = button("Add Toast").on_press_maybe( + (!self.editing.body.is_empty() && !self.editing.title.is_empty()) + .then_some(Message::Add), + ); let content = container( column![ @@ -173,6 +158,12 @@ impl Application for App { } } +impl Default for App { + fn default() -> Self { + Self::new() + } +} + mod toast { use std::fmt; use std::time::{Duration, Instant}; @@ -210,27 +201,6 @@ mod toast { &[Self::Primary, Self::Secondary, Self::Success, Self::Danger]; } - impl container::StyleSheet for Status { - type Style = Theme; - - fn appearance(&self, theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - let pair = match self { - Status::Primary => palette.primary.weak, - Status::Secondary => palette.secondary.weak, - Status::Success => palette.success.weak, - Status::Danger => palette.danger.weak, - }; - - container::Appearance { - background: Some(pair.color.into()), - text_color: pair.text.into(), - ..Default::default() - } - } - } - impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -274,7 +244,7 @@ mod toast { container( row![ text(toast.title.as_str()), - horizontal_space(Length::Fill), + horizontal_space(), button("X") .on_press((on_close)(index)) .padding(3), @@ -283,14 +253,17 @@ mod toast { ) .width(Length::Fill) .padding(5) - .style( - theme::Container::Custom(Box::new(toast.status)) - ), + .style(match toast.status { + Status::Primary => primary, + Status::Secondary => secondary, + Status::Success => success, + Status::Danger => danger, + }), horizontal_rule(1), container(text(toast.body.as_str())) .width(Length::Fill) .padding(5) - .style(theme::Container::Box), + .style(container::rounded_box), ]) .max_width(200) .into() @@ -677,4 +650,48 @@ mod toast { Element::new(manager) } } + + fn styled(pair: theme::palette::Pair) -> container::Appearance { + container::Appearance { + background: Some(pair.color.into()), + text_color: pair.text.into(), + ..Default::default() + } + } + + fn primary( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { + let palette = theme.extended_palette(); + + styled(palette.primary.weak) + } + + fn secondary( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { + let palette = theme.extended_palette(); + + styled(palette.secondary.weak) + } + + fn success( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { + let palette = theme.extended_palette(); + + styled(palette.success.weak) + } + + fn danger( + theme: &Theme, + _status: container::Status, + ) -> container::Appearance { + let palette = theme.extended_palette(); + + styled(palette.danger.weak) + } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index eae127f7a4..7768c1d512 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,14 +1,11 @@ use iced::alignment::{self, Alignment}; -use iced::font::{self, Font}; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::{ self, button, checkbox, column, container, keyed_column, row, scrollable, text, text_input, Text, }; use iced::window; -use iced::{Application, Element}; -use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced::{Command, Element, Font, Length, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -20,17 +17,17 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - Todos::run(Settings { - window: window::Settings { - size: Size::new(500.0, 800.0), - ..window::Settings::default() - }, - ..Settings::default() - }) + iced::program(Todos::title, Todos::update, Todos::view) + .load(Todos::load) + .subscription(Todos::subscription) + .font(include_bytes!("../fonts/icons.ttf").as_slice()) + .window_size((500.0, 800.0)) + .run() } -#[derive(Debug)] +#[derive(Default, Debug)] enum Todos { + #[default] Loading, Loaded(State), } @@ -47,7 +44,6 @@ struct State { #[derive(Debug, Clone)] enum Message { Loaded(Result), - FontLoaded(Result<(), font::Error>), Saved(Result<(), SaveError>), InputChanged(String), CreateTask, @@ -57,21 +53,9 @@ enum Message { ToggleFullscreen(window::Mode), } -impl Application for Todos { - type Message = Message; - type Theme = Theme; - type Executor = iced::executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (Todos, Command) { - ( - Todos::Loading, - Command::batch(vec![ - font::load(include_bytes!("../fonts/icons.ttf").as_slice()) - .map(Message::FontLoaded), - Command::perform(SavedState::load(), Message::Loaded), - ]), - ) +impl Todos { + fn load() -> Command { + Command::perform(SavedState::load(), Message::Loaded) } fn title(&self) -> String { @@ -168,7 +152,7 @@ impl Application for Todos { Message::ToggleFullscreen(mode) => { window::change_mode(window::Id::MAIN, mode) } - _ => Command::none(), + Message::Loaded(_) => Command::none(), }; if !saved { @@ -209,7 +193,7 @@ impl Application for Todos { let title = text("todos") .width(Length::Fill) .size(100) - .style(Color::from([0.5, 0.5, 0.5])) + .color([0.5, 0.5, 0.5]) .horizontal_alignment(alignment::Horizontal::Center); let input = text_input("What needs to be done?", input_value) @@ -355,6 +339,7 @@ impl Task { let checkbox = checkbox(&self.description, self.completed) .on_toggle(TaskMessage::Completed) .width(Length::Fill) + .size(17) .text_shaping(text::Shaping::Advanced); row![ @@ -362,7 +347,7 @@ impl Task { button(edit_icon()) .on_press(TaskMessage::Edit) .padding(10) - .style(theme::Button::Text), + .style(button::text), ] .spacing(20) .align_items(Alignment::Center) @@ -385,7 +370,7 @@ impl Task { ) .on_press(TaskMessage::Delete) .padding(10) - .style(theme::Button::Destructive) + .style(button::danger) ] .spacing(20) .align_items(Alignment::Center) @@ -402,9 +387,9 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { let label = text(label); let button = button(label).style(if filter == current_filter { - theme::Button::Primary + button::primary } else { - theme::Button::Text + button::text }); button.on_press(Message::FilterChanged(filter)).padding(8) @@ -467,7 +452,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { .width(Length::Fill) .size(25) .horizontal_alignment(alignment::Horizontal::Center) - .style(Color::from([0.7, 0.7, 0.7])), + .color([0.7, 0.7, 0.7]), ) .height(200) .center_y() diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index a904cce07f..b660306856 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -1,13 +1,13 @@ -use iced::theme; use iced::widget::tooltip::Position; use iced::widget::{button, container, tooltip}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::{Element, Length}; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::run("Tooltip - Iced", Tooltip::update, Tooltip::view) } -struct Example { +#[derive(Default)] +struct Tooltip { position: Position, } @@ -16,28 +16,16 @@ enum Message { ChangePosition, } -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Self { - position: Position::Bottom, - } - } - - fn title(&self) -> String { - String::from("Tooltip - Iced") - } - +impl Tooltip { 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, + Position::FollowCursor => Position::Top, }; self.position = position; @@ -53,7 +41,7 @@ impl Sandbox for Example { self.position, ) .gap(10) - .style(theme::Container::Box); + .style(container::rounded_box); container(tooltip) .width(Length::Fill) diff --git a/examples/tour/index.html b/examples/tour/index.html index c64af912c5..09d033679b 100644 --- a/examples/tour/index.html +++ b/examples/tour/index.html @@ -1,12 +1,12 @@ - + Tour - Iced - + diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 6d24b5ec9c..a88c0dba0f 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,11 +1,10 @@ use iced::alignment::{self, Alignment}; -use iced::theme; use iced::widget::{ - checkbox, column, container, horizontal_space, image, radio, row, + button, 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, Font, Length, Pixels, Sandbox, Settings}; +use iced::{Color, Element, Font, Length, Pixels}; pub fn main() -> iced::Result { #[cfg(target_arch = "wasm32")] @@ -17,24 +16,18 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - Tour::run(Settings::default()) + iced::program(Tour::title, Tour::update, Tour::view) + .centered() + .run() } +#[derive(Default)] pub struct Tour { steps: Steps, debug: bool, } -impl Sandbox for Tour { - type Message = Message; - - fn new() -> Tour { - Tour { - steps: Steps::new(), - debug: false, - } - } - +impl Tour { fn title(&self) -> String { format!("{} - Iced", self.steps.title()) } @@ -56,22 +49,17 @@ impl Sandbox for Tour { fn view(&self) -> Element { let Tour { steps, .. } = self; - let mut controls = row![]; - - if steps.has_previous() { - controls = controls.push( - button("Back") - .on_press(Message::BackPressed) - .style(theme::Button::Secondary), - ); - } - - controls = controls.push(horizontal_space(Length::Fill)); - - if steps.can_continue() { - controls = - controls.push(button("Next").on_press(Message::NextPressed)); - } + let controls = + row![] + .push_maybe(steps.has_previous().then(|| { + padded_button("Back") + .on_press(Message::BackPressed) + .style(button::secondary) + })) + .push(horizontal_space()) + .push_maybe(steps.can_continue().then(|| { + padded_button("Next").on_press(Message::NextPressed) + })); let content: Element<_> = column![ steps.view(self.debug).map(Message::StepMessage), @@ -177,6 +165,12 @@ impl Steps { } } +impl Default for Steps { + fn default() -> Self { + Steps::new() + } +} + enum Step { Welcome, Slider { @@ -478,7 +472,7 @@ impl<'a> Step { let color_section = column![ "And its color:", - text(format!("{color:?}")).style(color), + text(format!("{color:?}")).color(color), color_sliders, ] .padding(20) @@ -574,14 +568,14 @@ impl<'a> Step { text("Tip: You can use the scrollbar to scroll down faster!") .size(16), ) - .push(vertical_space(4096)) + .push(vertical_space().height(4096)) .push( text("You are halfway there!") .width(Length::Fill) .size(30) .horizontal_alignment(alignment::Horizontal::Center), ) - .push(vertical_space(4096)) + .push(vertical_space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) .push( text("You made it!") @@ -613,11 +607,7 @@ impl<'a> Step { Self::container("Text input") .push("Use a text input to ask for different kinds of information.") - .push(if is_secure { - text_input.password() - } else { - text_input - }) + .push(text_input.secure(is_secure)) .push( checkbox("Enable password mode", is_secure) .on_toggle(StepMessage::ToggleSecureInput), @@ -651,17 +641,10 @@ impl<'a> Step { "Give it a shot! Check the following checkbox to be able to \ see element boundaries.", ) - .push(if cfg!(target_arch = "wasm32") { - Element::new( - text("Not available on web yet!") - .style(Color::from([0.7, 0.7, 0.7])) - .horizontal_alignment(alignment::Horizontal::Center), - ) - } else { + .push( checkbox("Explain layout", debug) - .on_toggle(StepMessage::DebugToggled) - .into() - }) + .on_toggle(StepMessage::DebugToggled), + ) .push("Feel free to go back and take a look.") } @@ -691,8 +674,8 @@ fn ferris<'a>( .center_x() } -fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button(text(label)).padding([12, 24]) +fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + button(text(label)).padding([12, 24]) } fn color_slider<'a>( diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index bf57012319..df705b6c1f 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,12 +1,11 @@ use iced::event::{self, Event}; -use iced::executor; use iced::widget::{container, text}; -use iced::{ - Application, Command, Element, Length, Settings, Subscription, Theme, -}; +use iced::{Element, Length, Subscription}; pub fn main() -> iced::Result { - App::run(Settings::default()) + iced::program("URL Handler - Iced", App::update, App::view) + .subscription(App::subscription) + .run() } #[derive(Debug, Default)] @@ -19,21 +18,8 @@ enum Message { EventOccurred(Event), } -impl Application for App { - type Message = Message; - type Theme = Theme; - type Executor = executor::Default; - type Flags = (); - - fn new(_flags: ()) -> (App, Command) { - (App::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Url - Iced") - } - - fn update(&mut self, message: Message) -> Command { +impl App { + fn update(&mut self, message: Message) { match message { Message::EventOccurred(event) => { if let Event::PlatformSpecific( @@ -45,9 +31,7 @@ impl Application for App { self.url = Some(url); } } - }; - - Command::none() + } } fn subscription(&self) -> Subscription { diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index c2212b22ca..a7391e23f6 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -3,18 +3,20 @@ use iced::mouse; use iced::widget::{ canvas, checkbox, column, horizontal_space, row, slider, text, }; -use iced::{ - Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme, - Vector, -}; +use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { - VectorialText::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program( + "Vectorial Text - Iced", + VectorialText::update, + VectorialText::view, + ) + .theme(|_| Theme::Dark) + .antialiasing(true) + .run() } +#[derive(Default)] struct VectorialText { state: State, } @@ -27,19 +29,7 @@ enum Message { ToggleJapanese(bool), } -impl Sandbox for VectorialText { - type Message = Message; - - fn new() -> Self { - Self { - state: State::new(), - } - } - - fn title(&self) -> String { - String::from("Vectorial Text - Iced") - } - +impl VectorialText { fn update(&mut self, message: Message) { match message { Message::SizeChanged(size) => { @@ -64,7 +54,7 @@ impl Sandbox for VectorialText { column![ row![ text(label), - horizontal_space(Length::Fill), + horizontal_space(), text(format!("{:.2}", value)) ], slider(range, value, message).step(0.01) @@ -106,10 +96,6 @@ impl Sandbox for VectorialText { .padding(20) .into() } - - fn theme(&self) -> Theme { - Theme::Dark - } } struct State { @@ -170,3 +156,9 @@ impl canvas::Program for State { vec![geometry] } } + +impl Default for State { + fn default() -> Self { + State::new() + } +} diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 33d76da842..332b6a7b7f 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,20 +1,22 @@ use iced::event::{self, Event}; -use iced::executor; use iced::mouse; -use iced::theme::{self, Theme}; use iced::widget::{ column, container, horizontal_space, row, scrollable, text, vertical_space, }; use iced::window; use iced::{ - Alignment, Application, Color, Command, Element, Font, Length, Point, - Rectangle, Settings, Subscription, + Alignment, Color, Command, Element, Font, Length, Point, Rectangle, + Subscription, Theme, }; pub fn main() -> iced::Result { - Example::run(Settings::default()) + iced::program("Visible Bounds - Iced", Example::update, Example::view) + .subscription(Example::subscription) + .theme(|_| Theme::Dark) + .run() } +#[derive(Default)] struct Example { mouse_position: Option, outer_bounds: Option, @@ -30,27 +32,7 @@ enum Message { InnerBoundsFetched(Option), } -impl Application for Example { - type Message = Message; - type Theme = Theme; - type Flags = (); - type Executor = executor::Default; - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self { - mouse_position: None, - outer_bounds: None, - inner_bounds: None, - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Visible bounds - Iced") - } - +impl Example { fn update(&mut self, message: Message) -> Command { match message { Message::MouseMoved(position) => { @@ -81,8 +63,11 @@ impl Application for Example { let data_row = |label, value, color| { row![ text(label), - horizontal_space(Length::Fill), - text(value).font(Font::MONOSPACE).size(14).style(color), + horizontal_space(), + text(value) + .font(Font::MONOSPACE) + .size(14) + .color_maybe(color), ] .height(40) .align_items(Alignment::Center) @@ -102,13 +87,12 @@ impl Application for Example { }) .unwrap_or_default() { - Color { + Some(Color { g: 1.0, ..Color::BLACK - } - .into() + }) } else { - theme::Text::Default + None }, ) }; @@ -120,28 +104,28 @@ impl Application for Example { Some(Point { x, y }) => format!("({x}, {y})"), None => "unknown".to_string(), }, - theme::Text::Default, + None, ), view_bounds("Outer container", self.outer_bounds), view_bounds("Inner container", self.inner_bounds), scrollable( column![ text("Scroll me!"), - vertical_space(400), + vertical_space().height(400), container(text("I am the outer container!")) .id(OUTER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), - vertical_space(400), + .style(container::rounded_box), + vertical_space().height(400), scrollable( column![ text("Scroll me!"), - vertical_space(400), + vertical_space().height(400), container(text("I am the inner container!")) .id(INNER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), - vertical_space(400) + .style(container::rounded_box), + vertical_space().height(400), ] .padding(20) ) @@ -171,10 +155,6 @@ impl Application for Example { _ => None, }) } - - fn theme(&self) -> Theme { - Theme::Dark - } } use once_cell::sync::Lazy; diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 38a6db1e14..460d9a0839 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -1,17 +1,17 @@ mod echo; use iced::alignment::{self, Alignment}; -use iced::executor; use iced::widget::{ button, column, container, row, scrollable, text, text_input, }; -use iced::{ - Application, Color, Command, Element, Length, Settings, Subscription, Theme, -}; +use iced::{color, Command, Element, Length, Subscription}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { - WebSocket::run(Settings::default()) + iced::program("WebSocket - Iced", WebSocket::update, WebSocket::view) + .load(WebSocket::load) + .subscription(WebSocket::subscription) + .run() } #[derive(Default)] @@ -29,21 +29,9 @@ enum Message { Server, } -impl Application for WebSocket { - type Message = Message; - type Theme = Theme; - type Flags = (); - type Executor = executor::Default; - - fn new(_flags: Self::Flags) -> (Self, Command) { - ( - Self::default(), - Command::perform(echo::server::run(), |_| Message::Server), - ) - } - - fn title(&self) -> String { - String::from("WebSocket - Iced") +impl WebSocket { + fn load() -> Command { + Command::perform(echo::server::run(), |_| Message::Server) } fn update(&mut self, message: Message) -> Command { @@ -99,7 +87,7 @@ impl Application for WebSocket { let message_log: Element<_> = if self.messages.is_empty() { container( text("Your messages will appear here...") - .style(Color::from_rgb8(0x88, 0x88, 0x88)), + .color(color!(0x888888)), ) .width(Length::Fill) .height(Length::Fill) diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs index 2666f1b42d..ff7ea0f6b1 100644 --- a/futures/src/backend/wasm/wasm_bindgen.rs +++ b/futures/src/backend/wasm/wasm_bindgen.rs @@ -1,4 +1,4 @@ -//! A `wasm-bindgein-futures` backend. +//! A `wasm-bindgen-futures` backend. /// A `wasm-bindgen-futures` executor. #[derive(Debug)] diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index e32227f6e6..7537c022ad 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -138,9 +138,9 @@ impl std::fmt::Debug for Subscription { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.10/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.10/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.10/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. @@ -378,7 +378,7 @@ where /// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` /// connection open. /// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.10/examples/websocket +/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket pub fn channel( id: I, size: usize, diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 907f3705ae..0ee6ff47de 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -18,6 +18,7 @@ all-features = true geometry = ["lyon_path"] image = ["dep:image", "kamadak-exif"] web-colors = [] +fira-sans = [] [dependencies] iced_core.workspace = true diff --git a/graphics/fonts/FiraSans-Regular.ttf b/graphics/fonts/FiraSans-Regular.ttf new file mode 100644 index 0000000000..6f80647494 Binary files /dev/null and b/graphics/fonts/FiraSans-Regular.ttf differ diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0188f4d8b2..91951a8ed6 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -6,6 +6,7 @@ use crate::core::Color; use crate::futures::{MaybeSend, MaybeSync}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::future::Future; use thiserror::Error; /// A graphics compositor that can draw to windows. @@ -23,7 +24,7 @@ pub trait Compositor: Sized { fn new( settings: Self::Settings, compatible_window: W, - ) -> Result; + ) -> impl Future>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. fn create_renderer(&self) -> Self::Renderer; diff --git a/graphics/src/error.rs b/graphics/src/error.rs index 77758f5412..c6ea98a332 100644 --- a/graphics/src/error.rs +++ b/graphics/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { #[error("a suitable graphics adapter or device could not be found")] GraphicsAdapterNotFound, - /// An error occured in the context's internal backend - #[error("an error occured in the context's internal backend")] + /// An error occurred in the context's internal backend + #[error("an error occurred in the context's internal backend")] BackendError(String), } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 217a23e2e4..0310ead788 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -17,6 +17,15 @@ use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock, Weak}; +/// The regular variant of the [Fira Sans] font. +/// +/// It is loaded as part of the default fonts in Wasm builds. +/// +/// [Fira Sans]: https://mozilla.github.io/Fira/ +#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] +pub const FIRA_SANS_REGULAR: &'static [u8] = + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(); + /// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock { static FONT_SYSTEM: OnceCell> = OnceCell::new(); @@ -27,6 +36,10 @@ pub fn font_system() -> &'static RwLock { cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), )), + #[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] + cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(), + )), ]), version: Version::default(), }) diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index a159978c27..5cce24279e 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -18,6 +18,7 @@ geometry = ["iced_graphics/geometry", "iced_tiny_skia/geometry", "iced_wgpu?/geo tracing = ["iced_wgpu?/tracing"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] +fira-sans = ["iced_graphics/fira-sans"] [dependencies] iced_graphics.workspace = true diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index dc2c50ffd5..c23a814c63 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -4,6 +4,7 @@ use crate::graphics::{Error, Viewport}; use crate::{Renderer, Settings}; use std::env; +use std::future::Future; pub enum Compositor { TinySkia(iced_tiny_skia::window::Compositor), @@ -25,22 +26,25 @@ impl crate::graphics::Compositor for Compositor { fn new( settings: Self::Settings, compatible_window: W, - ) -> Result { + ) -> impl Future> { let candidates = Candidate::list_from_env().unwrap_or(Candidate::default_list()); - let mut error = Error::GraphicsAdapterNotFound; + async move { + let mut error = Error::GraphicsAdapterNotFound; - for candidate in candidates { - match candidate.build(settings, compatible_window.clone()) { - Ok(compositor) => return Ok(compositor), - Err(new_error) => { - error = new_error; + for candidate in candidates { + match candidate.build(settings, compatible_window.clone()).await + { + Ok(compositor) => return Ok(compositor), + Err(new_error) => { + error = new_error; + } } } - } - Err(error) + Err(error) + } } fn create_renderer(&self) -> Self::Renderer { @@ -225,7 +229,7 @@ impl Candidate { ) } - fn build( + async fn build( self, settings: Settings, _compatible_window: W, @@ -252,7 +256,8 @@ impl Candidate { ..iced_wgpu::Settings::from_env() }, _compatible_window, - )?; + ) + .await?; Ok(Compositor::Wgpu(compositor)) } diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs index f09ccfbfa6..36435148d9 100644 --- a/renderer/src/geometry.rs +++ b/renderer/src/geometry.rs @@ -2,7 +2,7 @@ mod cache; pub use cache::Cache; -use crate::core::{Point, Rectangle, Size, Transformation, Vector}; +use crate::core::{Point, Radians, Rectangle, Size, Transformation, Vector}; use crate::graphics::geometry::{Fill, Path, Stroke, Text}; use crate::Renderer; @@ -184,7 +184,7 @@ impl Frame { /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] - pub fn rotate(&mut self, angle: f32) { + pub fn rotate(&mut self, angle: impl Into) { delegate!(self, frame, frame.rotate(angle)); } diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index bc45091266..dd47c47d63 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,5 +1,6 @@ //! Access the clipboard. use crate::command::{self, Command}; +use crate::core::clipboard::Kind; use crate::futures::MaybeSend; use std::fmt; @@ -9,10 +10,10 @@ use std::fmt; /// [`Command`]: crate::Command pub enum Action { /// Read the clipboard and produce `T` with the result. - Read(Box) -> T>), + Read(Box) -> T>, Kind), /// Write the given contents to the clipboard. - Write(String), + Write(String, Kind), } impl Action { @@ -25,8 +26,10 @@ impl Action { T: 'static, { match self { - Self::Read(o) => Action::Read(Box::new(move |s| f(o(s)))), - Self::Write(content) => Action::Write(content), + Self::Read(o, target) => { + Action::Read(Box::new(move |s| f(o(s))), target) + } + Self::Write(content, target) => Action::Write(content, target), } } } @@ -34,8 +37,8 @@ impl Action { impl fmt::Debug for Action { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Read(_) => write!(f, "Action::Read"), - Self::Write(_) => write!(f, "Action::Write"), + Self::Read(_, target) => write!(f, "Action::Read{target:?}"), + Self::Write(_, target) => write!(f, "Action::Write({target:?})"), } } } @@ -44,10 +47,34 @@ impl fmt::Debug for Action { pub fn read( f: impl Fn(Option) -> Message + 'static, ) -> Command { - Command::single(command::Action::Clipboard(Action::Read(Box::new(f)))) + Command::single(command::Action::Clipboard(Action::Read( + Box::new(f), + Kind::Standard, + ))) +} + +/// Read the current contents of the primary clipboard. +pub fn read_primary( + f: impl Fn(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Clipboard(Action::Read( + Box::new(f), + Kind::Primary, + ))) } /// Write the given contents to the clipboard. pub fn write(contents: String) -> Command { - Command::single(command::Action::Clipboard(Action::Write(contents))) + Command::single(command::Action::Clipboard(Action::Write( + contents, + Kind::Standard, + ))) +} + +/// Write the given contents to the primary clipboard. +pub fn write_primary(contents: String) -> Command { + Command::single(command::Action::Clipboard(Action::Write( + contents, + Kind::Primary, + ))) } diff --git a/runtime/src/command.rs b/runtime/src/command.rs index f70da915fb..f7a746feea 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -112,6 +112,12 @@ impl Command { } } +impl From<()> for Command { + fn from(_value: ()) -> Self { + Self::none() + } +} + impl fmt::Debug for Command { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Command(command) = self; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 03906f459a..5c2836a5bc 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,10 +1,10 @@ //! A renderer-agnostic native GUI runtime. //! -//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/raw/improvement/update-ecosystem-and-roadmap/docs/graphs/native.png) +//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/master/docs/graphs/native.png?raw=true) //! //! `iced_runtime` takes [`iced_core`] and builds a native runtime on top of it. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.10/core +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.12/core #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 08431cedfa..748fb651ea 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -19,7 +19,7 @@ use crate::overlay; /// The [`integration`] example uses a [`UserInterface`] to integrate Iced in an /// existing graphical application. /// -/// [`integration`]: https://github.com/iced-rs/iced/tree/0.10/examples/integration +/// [`integration`]: https://github.com/iced-rs/iced/tree/0.12/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Theme, Renderer> { root: Element<'a, Message, Theme, Renderer>, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 4d97d5eec9..24171e3ee2 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -101,6 +101,17 @@ pub fn minimize(id: Id, minimized: bool) -> Command { Command::single(command::Action::Window(Action::Minimize(id, minimized))) } +/// Fetches the current window position in logical coordinates. +pub fn fetch_position( + id: Id, + f: impl FnOnce(Option) -> Message + 'static, +) -> Command { + Command::single(command::Action::Window(Action::FetchPosition( + id, + Box::new(f), + ))) +} + /// Moves the window to the given logical coordinates. pub fn move_to(id: Id, position: Point) -> Command { Command::single(command::Action::Window(Action::Move(id, position))) @@ -160,6 +171,13 @@ pub fn change_level(id: Id, level: Level) -> Command { Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } +/// Show the [system menu] at cursor position. +/// +/// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu +pub fn show_system_menu(id: Id) -> Command { + Command::single(command::Action::Window(Action::ShowSystemMenu(id))) +} + /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. pub fn fetch_id( diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 86d5852880..e44ff5a688 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -38,6 +38,8 @@ pub enum Action { FetchMinimized(Id, Box) -> T + 'static>), /// Set the window to minimized or back Minimize(Id, bool), + /// Fetch the current logical coordinates of the window. + FetchPosition(Id, Box) -> T + 'static>), /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. @@ -81,6 +83,11 @@ pub enum Action { GainFocus(Id), /// Change the window [`Level`]. ChangeLevel(Id, Level), + /// Show the system menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. + ShowSystemMenu(Id), /// Fetch the raw identifier unique to the window. FetchId(Id, Box T + 'static>), /// Change the window [`Icon`]. @@ -129,6 +136,9 @@ impl Action { Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) } Self::Minimize(id, minimized) => Action::Minimize(id, minimized), + Self::FetchPosition(id, o) => { + Action::FetchPosition(id, Box::new(move |s| f(o(s)))) + } Self::Move(id, position) => Action::Move(id, position), Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), Self::FetchMode(id, o) => { @@ -141,6 +151,7 @@ impl Action { } Self::GainFocus(id) => Action::GainFocus(id), Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), + Self::ShowSystemMenu(id) => Action::ShowSystemMenu(id), Self::FetchId(id, o) => { Action::FetchId(id, Box::new(move |s| f(o(s)))) } @@ -180,6 +191,9 @@ impl fmt::Debug for Action { Self::Minimize(id, minimized) => { write!(f, "Action::Minimize({id:?}, {minimized}") } + Self::FetchPosition(id, _) => { + write!(f, "Action::FetchPosition({id:?})") + } Self::Move(id, position) => { write!(f, "Action::Move({id:?}, {position})") } @@ -200,6 +214,9 @@ impl fmt::Debug for Action { Self::ChangeLevel(id, level) => { write!(f, "Action::ChangeLevel({id:?}, {level:?})") } + Self::ShowSystemMenu(id) => { + write!(f, "Action::ShowSystemMenu({id:?})") + } Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), Self::ChangeIcon(id, _icon) => { write!(f, "Action::ChangeIcon({id:?})") diff --git a/src/advanced.rs b/src/advanced.rs index 2071aed04b..306c3559be 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,4 +1,6 @@ //! Leverage advanced concepts like custom widgets. +pub use crate::application::Application; +pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; pub use crate::core::mouse; @@ -7,7 +9,7 @@ pub use crate::core::renderer::{self, Renderer}; pub use crate::core::svg; pub use crate::core::text::{self, Text}; pub use crate::core::widget::{self, Widget}; -pub use crate::core::{Clipboard, Hasher, Shell}; +pub use crate::core::{Hasher, Shell}; pub use crate::renderer::graphics; pub mod subscription { diff --git a/src/application.rs b/src/application.rs index 01b2032f43..8317abcbc8 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,7 +1,8 @@ //! Build interactive cross-platform applications. +use crate::shell::application; use crate::{Command, Element, Executor, Settings, Subscription}; -pub use crate::style::application::{Appearance, StyleSheet}; +pub use application::{Appearance, DefaultStyle}; /// An interactive cross-platform application. /// @@ -13,9 +14,7 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// document. /// /// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. If you do not intend to perform any -/// background work in your program, the [`Sandbox`] trait offers a simplified -/// interface. +/// [`Command`] in some of its methods. /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. @@ -39,15 +38,15 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// to listen to time. /// - [`todos`], a todos tracker inspired by [TodoMVC]. /// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.10/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.10/examples/clock -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.10/examples/download_progress -/// [`events`]: https://github.com/iced-rs/iced/tree/0.10/examples/events -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.10/examples/game_of_life -/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.10/examples/pokedex -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.10/examples/solar_system -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.10/examples/stopwatch -/// [`todos`]: https://github.com/iced-rs/iced/tree/0.10/examples/todos +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.12/examples +/// [`clock`]: https://github.com/iced-rs/iced/tree/0.12/examples/clock +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress +/// [`events`]: https://github.com/iced-rs/iced/tree/0.12/examples/events +/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.12/examples/game_of_life +/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.12/examples/pokedex +/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.12/examples/solar_system +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch +/// [`todos`]: https://github.com/iced-rs/iced/tree/0.12/examples/todos /// [`Sandbox`]: crate::Sandbox /// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ @@ -59,8 +58,9 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// says "Hello, world!": /// /// ```no_run +/// use iced::advanced::Application; /// use iced::executor; -/// use iced::{Application, Command, Element, Settings, Theme}; +/// use iced::{Command, Element, Settings, Theme}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -91,7 +91,10 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// } /// } /// ``` -pub trait Application: Sized { +pub trait Application: Sized +where + Self::Theme: DefaultStyle, +{ /// The [`Executor`] that will run commands and subscriptions. /// /// The [default executor] can be a good starting point! @@ -104,7 +107,7 @@ pub trait Application: Sized { type Message: std::fmt::Debug + Send; /// The theme of your [`Application`]. - type Theme: Default + StyleSheet; + type Theme: Default; /// The data needed to initialize your [`Application`]. type Flags; @@ -148,11 +151,9 @@ pub trait Application: Sized { Self::Theme::default() } - /// Returns the current `Style` of the [`Theme`]. - /// - /// [`Theme`]: Self::Theme - fn style(&self) -> ::Style { - ::Style::default() + /// Returns the current [`Appearance`] of the [`Application`]. + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// Returns the event [`Subscription`] for the current state of the @@ -205,19 +206,38 @@ pub trait Application: Sized { ..crate::renderer::Settings::default() }; - Ok(crate::shell::application::run::< + let run = crate::shell::application::run::< Instance, Self::Executor, crate::renderer::Compositor, - >(settings.into(), renderer_settings)?) + >(settings.into(), renderer_settings); + + #[cfg(target_arch = "wasm32")] + { + use crate::futures::FutureExt; + use iced_futures::backend::wasm::wasm_bindgen::Executor; + + Executor::new() + .expect("Create Wasm executor") + .spawn(run.map(|_| ())); + + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + Ok(crate::futures::executor::block_on(run)?) } } -struct Instance(A); +struct Instance(A) +where + A: Application, + A::Theme: DefaultStyle; impl crate::runtime::Program for Instance where A: Application, + A::Theme: DefaultStyle, { type Message = A::Message; type Theme = A::Theme; @@ -232,9 +252,10 @@ where } } -impl crate::shell::Application for Instance +impl application::Application for Instance where A: Application, + A::Theme: DefaultStyle, { type Flags = A::Flags; @@ -252,8 +273,8 @@ where self.0.theme() } - fn style(&self) -> ::Style { - self.0.style() + fn style(&self, theme: &A::Theme) -> Appearance { + self.0.style(theme) } fn subscription(&self) -> Subscription { diff --git a/src/lib.rs b/src/lib.rs index 3cd145f848..0e9566e2b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,13 +24,13 @@ //! [scrollables]: https://iced.rs/examples/scrollable.mp4 //! [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4 //! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.10/runtime +//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.12/runtime //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.10/wgpu -//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.10/winit +//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.12/wgpu +//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.12/winit //! [`dodrio`]: https://github.com/fitzgen/dodrio //! [web runtime]: https://github.com/iced-rs/iced_web -//! [examples]: https://github.com/iced-rs/iced/tree/0.10/examples +//! [examples]: https://github.com/iced-rs/iced/tree/0.12/examples //! [repository]: https://github.com/iced-rs/iced //! //! # Overview @@ -63,8 +63,8 @@ //! ``` //! #[derive(Debug, Clone, Copy)] //! pub enum Message { -//! IncrementPressed, -//! DecrementPressed, +//! Increment, +//! Decrement, //! } //! ``` //! @@ -79,8 +79,8 @@ //! # //! # #[derive(Debug, Clone, Copy)] //! # pub enum Message { -//! # IncrementPressed, -//! # DecrementPressed, +//! # Increment, +//! # Decrement, //! # } //! # //! use iced::widget::{button, column, text, Column}; @@ -90,15 +90,15 @@ //! // We use a column: a simple vertical layout //! column![ //! // The increment button. We tell it to produce an -//! // `IncrementPressed` message when pressed -//! button("+").on_press(Message::IncrementPressed), +//! // `Increment` message when pressed +//! button("+").on_press(Message::Increment), //! //! // 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), +//! // `Decrement` message when pressed +//! button("-").on_press(Message::Decrement), //! ] //! } //! } @@ -115,18 +115,18 @@ //! # //! # #[derive(Debug, Clone, Copy)] //! # pub enum Message { -//! # IncrementPressed, -//! # DecrementPressed, +//! # Increment, +//! # Decrement, //! # } //! impl Counter { //! // ... //! //! pub fn update(&mut self, message: Message) { //! match message { -//! Message::IncrementPressed => { +//! Message::Increment => { //! self.value += 1; //! } -//! Message::DecrementPressed => { +//! Message::Decrement => { //! self.value -= 1; //! } //! } @@ -134,8 +134,22 @@ //! } //! ``` //! -//! And that's everything! We just wrote a whole user interface. Iced is now -//! able to: +//! And that's everything! We just wrote a whole user interface. Let's run it: +//! +//! ```no_run +//! # #[derive(Default)] +//! # struct Counter; +//! # impl Counter { +//! # fn update(&mut self, _message: ()) {} +//! # fn view(&self) -> iced::Element<()> { unimplemented!() } +//! # } +//! # +//! fn main() -> iced::Result { +//! iced::run("A cool counter", Counter::update, Counter::view) +//! } +//! ``` +//! +//! Iced will automatically: //! //! 1. Take the result of our __view logic__ and layout its widgets. //! 1. Process events from our system and produce __messages__ for our @@ -143,11 +157,11 @@ //! 1. Draw the resulting user interface. //! //! # Usage -//! The [`Application`] and [`Sandbox`] traits should get you started quickly, -//! streamlining all the process described above! +//! Use [`run`] or the [`program`] builder. //! //! [Elm]: https://elm-lang.org/ //! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ +//! [`program`]: program() #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] @@ -162,7 +176,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] use iced_widget::graphics; use iced_widget::renderer; -use iced_widget::style; use iced_winit as shell; use iced_winit::core; use iced_winit::runtime; @@ -172,10 +185,10 @@ pub use iced_futures::futures; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; +mod application; mod error; -mod sandbox; -pub mod application; +pub mod program; pub mod settings; pub mod time; pub mod window; @@ -186,21 +199,22 @@ pub mod advanced; #[cfg(feature = "multi-window")] pub mod multi_window; -pub use style::theme; - pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; +pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, + Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme, Transformation, Vector, }; pub mod clipboard { //! Access the clipboard. - pub use crate::runtime::clipboard::{read, write}; + pub use crate::runtime::clipboard::{ + read, read_primary, write, write_primary, + }; } pub mod executor { @@ -302,17 +316,15 @@ pub mod widget { mod runtime {} } -pub use application::Application; pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; pub use font::Font; +pub use program::Program; pub use renderer::Renderer; -pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; -pub use theme::Theme; /// A generic widget. /// @@ -324,7 +336,54 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running an [`Application`]. -/// -/// [`Application`]: crate::Application +/// The result of running a [`Program`]. pub type Result = std::result::Result<(), Error>; + +/// Runs a basic iced application with default [`Settings`] given its title, +/// update, and view logic. +/// +/// This is equivalent to chaining [`program`] with [`Program::run`]. +/// +/// [`program`]: program() +/// +/// # Example +/// ```no_run +/// use iced::widget::{button, column, text, Column}; +/// +/// pub fn main() -> iced::Result { +/// iced::run("A counter", update, view) +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } +/// +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, +/// } +/// } +/// +/// fn view(value: &u64) -> Column { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] +/// } +/// ``` +pub fn run( + title: impl program::Title + 'static, + update: impl program::Update + 'static, + view: impl for<'a> program::View<'a, State, Message, Theme> + 'static, +) -> Result +where + State: Default + 'static, + Message: std::fmt::Debug + Send + 'static, + Theme: Default + program::DefaultStyle + 'static, +{ + program(title, update, view).run() +} + +#[doc(inline)] +pub use program::program; diff --git a/src/multi_window.rs b/src/multi_window.rs index 5b7a00b4db..fca0be4606 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -1,4 +1,254 @@ //! Leverage multi-window support in your application. -mod application; +use crate::window; +use crate::{Command, Element, Executor, Settings, Subscription}; -pub use application::Application; +pub use crate::application::{Appearance, DefaultStyle}; + +/// An interactive cross-platform multi-window application. +/// +/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`](#method.run). +/// +/// - On native platforms, it will run in its own windows. +/// - On the web, it will take control of the `` and the `<body>` of the +/// document and display only the contents of the `window::Id::MAIN` window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +/// +/// # Examples +/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. +/// +/// ## A simple "Hello, world!" +/// +/// If you just want to get started, here is a simple [`Application`] that +/// says "Hello, world!": +/// +/// ```no_run +/// use iced::{executor, window}; +/// use iced::{Command, Element, Settings, Theme}; +/// use iced::multi_window::{self, Application}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl multi_window::Application for Hello { +/// type Executor = executor::Default; +/// type Flags = (); +/// type Message = (); +/// type Theme = Theme; +/// +/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { +/// (Hello, Command::none()) +/// } +/// +/// fn title(&self, _window: window::Id) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { +/// Command::none() +/// } +/// +/// fn view(&self, _window: window::Id) -> Element<Self::Message> { +/// "Hello, world!".into() +/// } +/// } +/// ``` +/// +/// [`Sandbox`]: crate::Sandbox +pub trait Application: Sized +where + Self::Theme: DefaultStyle, +{ + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default; + + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Command`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + + /// Returns the current title of the `window` of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your window when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the `window` of the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + + /// Returns the current [`Theme`] of the `window` of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + #[allow(unused_variables)] + fn theme(&self, window: window::Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current `Style` of the [`Theme`]. + /// + /// [`Theme`]: Self::Theme + fn style(&self, theme: &Self::Theme) -> Appearance { + Self::Theme::default_style(theme) + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Returns the scale factor of the `window` of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } + + /// Runs the multi-window [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings<Self::Flags>) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::multi_window::run::< + Instance<Self>, + Self::Executor, + crate::renderer::Compositor, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance<A>(A) +where + A: Application, + A::Theme: DefaultStyle; + +impl<A> crate::runtime::multi_window::Program for Instance<A> +where + A: Application, + A::Theme: DefaultStyle, +{ + type Message = A::Message; + type Theme = A::Theme; + type Renderer = crate::Renderer; + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) + } + + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { + self.0.view(window) + } +} + +impl<A> crate::shell::multi_window::Application for Instance<A> +where + A: Application, + A::Theme: DefaultStyle, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: window::Id) -> String { + self.0.title(window) + } + + fn theme(&self, window: window::Id) -> A::Theme { + self.0.theme(window) + } + + fn style(&self, theme: &Self::Theme) -> Appearance { + self.0.style(theme) + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs deleted file mode 100644 index ac625281c6..0000000000 --- a/src/multi_window/application.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::style::application::StyleSheet; -use crate::window; -use crate::{Command, Element, Executor, Settings, Subscription}; - -/// An interactive cross-platform multi-window application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`](#method.run). -/// -/// - On native platforms, it will run in its own windows. -/// - On the web, it will take control of the `<title>` and the `<body>` of the -/// document and display only the contents of the `window::Id::MAIN` window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. If you do not intend to perform any -/// background work in your program, the [`Sandbox`] trait offers a simplified -/// interface. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": -/// -/// ```no_run -/// use iced::{executor, window}; -/// use iced::{Command, Element, Settings, Theme}; -/// use iced::multi_window::{self, Application}; -/// -/// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) -/// } -/// -/// struct Hello; -/// -/// impl multi_window::Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// -/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { -/// (Hello, Command::none()) -/// } -/// -/// fn title(&self, _window: window::Id) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { -/// Command::none() -/// } -/// -/// fn view(&self, _window: window::Id) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } -/// } -/// ``` -/// -/// [`Sandbox`]: crate::Sandbox -pub trait Application: Sized { - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::executor::Default - type Executor: Executor; - - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme of your [`Application`]. - type Theme: Default + StyleSheet; - - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Command`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - - /// Returns the current title of the `window` of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your window when necessary. - fn title(&self, window: window::Id) -> String; - - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; - - /// Returns the widgets to display in the `window` of the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; - - /// Returns the current [`Theme`] of the `window` of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - #[allow(unused_variables)] - fn theme(&self, window: window::Id) -> Self::Theme { - Self::Theme::default() - } - - /// Returns the current `Style` of the [`Theme`]. - /// - /// [`Theme`]: Self::Theme - fn style(&self) -> <Self::Theme as StyleSheet>::Style { - <Self::Theme as StyleSheet>::Style::default() - } - - /// Returns the event [`Subscription`] for the current state of the - /// application. - /// - /// A [`Subscription`] will be kept alive as long as you keep returning it, - /// and the __messages__ produced will be handled by - /// [`update`](#tymethod.update). - /// - /// By default, this method returns an empty [`Subscription`]. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the `window` of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - #[allow(unused_variables)] - fn scale_factor(&self, window: window::Id) -> f64 { - 1.0 - } - - /// Runs the multi-window [`Application`]. - /// - /// On native platforms, this method will take control of the current thread - /// until the [`Application`] exits. - /// - /// On the web platform, this method __will NOT return__ unless there is an - /// [`Error`] during startup. - /// - /// [`Error`]: crate::Error - fn run(settings: Settings<Self::Flags>) -> crate::Result - where - Self: 'static, - { - #[allow(clippy::needless_update)] - let renderer_settings = crate::renderer::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None - }, - ..crate::renderer::Settings::default() - }; - - Ok(crate::shell::multi_window::run::< - Instance<Self>, - Self::Executor, - crate::renderer::Compositor, - >(settings.into(), renderer_settings)?) - } -} - -struct Instance<A: Application>(A); - -impl<A> crate::runtime::multi_window::Program for Instance<A> -where - A: Application, -{ - type Message = A::Message; - type Theme = A::Theme; - type Renderer = crate::Renderer; - - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { - self.0.update(message) - } - - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view(window) - } -} - -impl<A> crate::shell::multi_window::Application for Instance<A> -where - A: Application, -{ - type Flags = A::Flags; - - fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn title(&self, window: window::Id) -> String { - self.0.title(window) - } - - fn theme(&self, window: window::Id) -> A::Theme { - self.0.theme(window) - } - - fn style(&self) -> <A::Theme as StyleSheet>::Style { - self.0.style() - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() - } - - fn scale_factor(&self, window: window::Id) -> f64 { - self.0.scale_factor(window) - } -} diff --git a/src/program.rs b/src/program.rs new file mode 100644 index 0000000000..7a36658506 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,851 @@ +//! Create and run iced applications step by step. +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::program("A counter", update, view) +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column<Message> { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::application::Application; +use crate::executor::{self, Executor}; +use crate::window; +use crate::{Command, Element, Font, Result, Settings, Size, Subscription}; + +pub use crate::application::{Appearance, DefaultStyle}; + +use std::borrow::Cow; + +/// Creates an iced [`Program`] given its title, update, and view logic. +/// +/// # Example +/// ```no_run +/// use iced::widget::{button, column, text, Column}; +/// +/// pub fn main() -> iced::Result { +/// iced::program("A counter", update, view).run() +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } +/// +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, +/// } +/// } +/// +/// fn view(value: &u64) -> Column<Message> { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] +/// } +/// ``` +pub fn program<State, Message, Theme>( + title: impl Title<State>, + update: impl Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme>, +) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> +where + State: 'static, + Message: Send + std::fmt::Debug, + Theme: Default + DefaultStyle, +{ + use std::marker::PhantomData; + + struct Application<State, Message, Theme, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + } + + impl<State, Message, Theme, Update, View> Definition + for Application<State, Message, Theme, Update, View> + where + Message: Send + std::fmt::Debug, + Theme: Default + DefaultStyle, + Update: self::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + + fn load(&self) -> Command<Self::Message> { + Command::none() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.view.view(state).into() + } + } + + Program { + raw: Application { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + }, + settings: Settings::default(), + } + .title(title) +} + +/// The underlying definition and configuration of an iced application. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`Program`] with the [`program`] helper. +/// +/// [`run`]: Program::run +#[derive(Debug)] +pub struct Program<P: Definition> { + raw: P, + settings: Settings, +} + +impl<P: Definition> Program<P> { + /// Runs the underlying [`Application`] of the [`Program`]. + /// + /// The state of the [`Program`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.run_with(P::State::default) + } + + /// Runs the underlying [`Application`] of the [`Program`] with a + /// closure that creates the initial state. + pub fn run_with( + self, + initialize: impl Fn() -> P::State + Clone + 'static, + ) -> Result + where + Self: 'static, + { + use std::marker::PhantomData; + + struct Instance<P: Definition, I> { + program: P, + state: P::State, + _initialize: PhantomData<I>, + } + + impl<P: Definition, I: Fn() -> P::State> Application for Instance<P, I> { + type Message = P::Message; + type Theme = P::Theme; + type Flags = (P, I); + type Executor = P::Executor; + + fn new( + (program, initialize): Self::Flags, + ) -> (Self, Command<Self::Message>) { + let state = initialize(); + let command = program.load(); + + ( + Self { + program, + state, + _initialize: PhantomData, + }, + command, + ) + } + + fn title(&self) -> String { + self.program.title(&self.state) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + ) -> crate::Element<'_, Self::Message, Self::Theme, crate::Renderer> + { + self.program.view(&self.state) + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.program.subscription(&self.state) + } + + fn theme(&self) -> Self::Theme { + self.program.theme(&self.state) + } + + fn style(&self, theme: &Self::Theme) -> Appearance { + self.program.style(&self.state, theme) + } + } + + let Self { raw, settings } = self; + + Instance::run(Settings { + flags: (raw, initialize), + id: settings.id, + window: settings.window, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + }) + } + + /// Sets the [`Settings`] that will be used to run the [`Program`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Program`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Program`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Program`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Program`]. + pub fn centered(self) -> Self { + Self { + settings: Settings { + window: window::Settings { + position: window::Position::Centered, + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`window::Settings::exit_on_close_request`] of the [`Program`]. + pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { + Self { + settings: Settings { + window: window::Settings { + exit_on_close_request, + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`window::Settings::size`] of the [`Program`]. + pub fn window_size(self, size: impl Into<Size>) -> Self { + Self { + settings: Settings { + window: window::Settings { + size: size.into(), + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`window::Settings::transparent`] of the [`Program`]. + pub fn transparent(self, transparent: bool) -> Self { + Self { + settings: Settings { + window: window::Settings { + transparent, + ..self.settings.window + }, + ..self.settings + }, + ..self + } + } + + /// Sets the [`Title`] of the [`Program`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Program< + impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Program { + raw: with_title(self.raw, title), + settings: self.settings, + } + } + + /// Runs the [`Command`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Command<P::Message>, + ) -> Program< + impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Program { + raw: with_load(self.raw, f), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Program`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Program< + impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Program { + raw: with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Program`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Program< + impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Program { + raw: with_theme(self.raw, f), + settings: self.settings, + } + } + + /// Sets the style logic of the [`Program`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Program< + impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Program { + raw: with_style(self.raw, f), + settings: self.settings, + } + } +} + +/// The internal definition of a [`Program`]. +/// +/// You should not need to implement this trait directly. Instead, use the +/// methods available in the [`Program`] struct. +#[allow(missing_docs)] +pub trait Definition: Sized { + /// The state of the program. + type State; + + /// The message of the program. + type Message: Send + std::fmt::Debug; + + /// The theme of the program. + type Theme: Default + DefaultStyle; + + /// The executor of the program. + type Executor: Executor; + + fn load(&self) -> Command<Self::Message>; + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message>; + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme>; + + fn title(&self, _state: &Self::State) -> String { + String::from("A cool iced application!") + } + + fn subscription( + &self, + _state: &Self::State, + ) -> Subscription<Self::Message> { + Subscription::none() + } + + fn theme(&self, _state: &Self::State) -> Self::Theme { + Self::Theme::default() + } + + fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { + DefaultStyle::default_style(theme) + } +} + +fn with_title<P: Definition>( + program: P, + title: impl Title<P::State>, +) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithTitle<P, Title> { + program: P, + title: Title, + } + + impl<P, Title> Definition for WithTitle<P, Title> + where + P: Definition, + Title: self::Title<P::State>, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = P::Executor; + + fn load(&self) -> Command<Self::Message> { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.title.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithTitle { program, title } +} + +fn with_load<P: Definition>( + program: P, + f: impl Fn() -> Command<P::Message>, +) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithLoad<P, F> { + program: P, + load: F, + } + + impl<P: Definition, F> Definition for WithLoad<P, F> + where + F: Fn() -> Command<P::Message>, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = executor::Default; + + fn load(&self) -> Command<Self::Message> { + Command::batch([self.program.load(), (self.load)()]) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithLoad { program, load: f } +} + +fn with_subscription<P: Definition>( + program: P, + f: impl Fn(&P::State) -> Subscription<P::Message>, +) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithSubscription<P, F> { + program: P, + subscription: F, + } + + impl<P: Definition, F> Definition for WithSubscription<P, F> + where + F: Fn(&P::State) -> Subscription<P::Message>, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = executor::Default; + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + (self.subscription)(state) + } + + fn load(&self) -> Command<Self::Message> { + self.program.load() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithSubscription { + program, + subscription: f, + } +} + +fn with_theme<P: Definition>( + program: P, + f: impl Fn(&P::State) -> P::Theme, +) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithTheme<P, F> { + program: P, + theme: F, + } + + impl<P: Definition, F> Definition for WithTheme<P, F> + where + F: Fn(&P::State) -> P::Theme, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = P::Executor; + + fn theme(&self, state: &Self::State) -> Self::Theme { + (self.theme)(state) + } + + fn load(&self) -> Command<Self::Message> { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithTheme { program, theme: f } +} + +fn with_style<P: Definition>( + program: P, + f: impl Fn(&P::State, &P::Theme) -> Appearance, +) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithStyle<P, F> { + program: P, + style: F, + } + + impl<P: Definition, F> Definition for WithStyle<P, F> + where + F: Fn(&P::State, &P::Theme) -> Appearance, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Executor = P::Executor; + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + (self.style)(state, theme) + } + + fn load(&self) -> Command<Self::Message> { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Command<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme> { + self.program.view(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + } + + WithStyle { program, style: f } +} + +/// The title logic of some [`Program`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// This trait allows the [`program`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Program`]. + fn title(&self, state: &State) -> String; +} + +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T +where + T: Fn(&State) -> String, +{ + fn title(&self, state: &State) -> String { + self(state) + } +} + +/// The update logic of some [`Program`]. +/// +/// This trait allows the [`program`] builder to take any closure that +/// returns any `Into<Command<Message>>`. +pub trait Update<State, Message> { + /// Processes the message and updates the state of the [`Program`]. + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Command<Message>>; +} + +impl<T, State, Message, C> Update<State, Message> for T +where + T: Fn(&mut State, Message) -> C, + C: Into<Command<Message>>, +{ + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Command<Message>> { + self(state, message) + } +} + +/// The view logic of some [`Program`]. +/// +/// This trait allows the [`program`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme> { + /// Produces the widget of the [`Program`]. + fn view(&self, state: &'a State) -> impl Into<Element<'a, Message, Theme>>; +} + +impl<'a, T, State, Message, Theme, Widget> View<'a, State, Message, Theme> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme>>, +{ + fn view(&self, state: &'a State) -> impl Into<Element<'a, Message, Theme>> { + self(state) + } +} diff --git a/src/sandbox.rs b/src/sandbox.rs deleted file mode 100644 index 825a0b60e2..0000000000 --- a/src/sandbox.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::theme::{self, Theme}; -use crate::{Application, Command, Element, Error, Settings, Subscription}; - -/// A sandboxed [`Application`]. -/// -/// If you are a just getting started with the library, this trait offers a -/// simpler interface than [`Application`]. -/// -/// Unlike an [`Application`], a [`Sandbox`] cannot run any asynchronous -/// actions or be initialized with some external flags. However, both traits -/// are very similar and upgrading from a [`Sandbox`] is very straightforward. -/// -/// Therefore, it is recommended to always start by implementing this trait and -/// upgrade only once necessary. -/// -/// # Examples -/// [The repository has a bunch of examples] that use the [`Sandbox`] trait: -/// -/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using the -/// [`Canvas widget`]. -/// - [`counter`], the classic counter example explained in [the overview]. -/// - [`custom_widget`], a demonstration of how to build a custom widget that -/// draws a circle. -/// - [`geometry`], a custom widget showcasing how to draw geometry with the -/// `Mesh2D` primitive in [`iced_wgpu`]. -/// - [`pane_grid`], a grid of panes that can be split, resized, and -/// reorganized. -/// - [`progress_bar`], a simple progress bar that can be filled by using a -/// slider. -/// - [`styling`], an example showcasing custom styling with a light and dark -/// theme. -/// - [`svg`], an application that renders the [Ghostscript Tiger] by leveraging -/// the [`Svg` widget]. -/// - [`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/iced-rs/iced/tree/0.10/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.10/examples/bezier_tool -/// [`counter`]: https://github.com/iced-rs/iced/tree/0.10/examples/counter -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.10/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.10/examples/geometry -/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.10/examples/pane_grid -/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.10/examples/progress_bar -/// [`styling`]: https://github.com/iced-rs/iced/tree/0.10/examples/styling -/// [`svg`]: https://github.com/iced-rs/iced/tree/0.10/examples/svg -/// [`tour`]: https://github.com/iced-rs/iced/tree/0.10/examples/tour -/// [`Canvas widget`]: crate::widget::Canvas -/// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.10/wgpu -/// [`Svg` widget]: crate::widget::Svg -/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Sandbox`] that -/// says "Hello, world!": -/// -/// ```no_run -/// use iced::{Element, Sandbox, Settings}; -/// -/// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) -/// } -/// -/// struct Hello; -/// -/// impl Sandbox for Hello { -/// type Message = (); -/// -/// fn new() -> Hello { -/// Hello -/// } -/// -/// fn title(&self) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) { -/// // This application has no interactions -/// } -/// -/// fn view(&self) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } -/// } -/// ``` -pub trait Sandbox { - /// The type of __messages__ your [`Sandbox`] will produce. - type Message: std::fmt::Debug + Send; - - /// Initializes the [`Sandbox`]. - /// - /// Here is where you should return the initial state of your app. - fn new() -> Self; - - /// Returns the current title of the [`Sandbox`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; - - /// Handles a __message__ and updates the state of the [`Sandbox`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by user interactions, will be handled by this method. - fn update(&mut self, message: Self::Message); - - /// Returns the widgets to display in the [`Sandbox`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message>; - - /// Returns the current [`Theme`] of the [`Sandbox`]. - /// - /// 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`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { - 1.0 - } - - /// Runs the [`Sandbox`]. - /// - /// On native platforms, this method will take control of the current thread - /// and __will NOT return__. - /// - /// It should probably be that last thing you call in your `main` function. - fn run(settings: Settings<()>) -> Result<(), Error> - where - Self: 'static + Sized, - { - <Self as Application>::run(settings) - } -} - -impl<T> Application for T -where - T: Sandbox, -{ - 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()) - } - - fn title(&self) -> String { - T::title(self) - } - - fn update(&mut self, message: T::Message) -> Command<T::Message> { - T::update(self, message); - - Command::none() - } - - fn view(&self) -> Element<'_, T::Message> { - T::view(self) - } - - fn theme(&self) -> Self::Theme { - T::theme(self) - } - - fn style(&self) -> theme::Application { - T::style(self) - } - - fn subscription(&self) -> Subscription<T::Message> { - Subscription::none() - } - - fn scale_factor(&self) -> f64 { - T::scale_factor(self) - } -} diff --git a/src/settings.rs b/src/settings.rs index d9476b614e..f794784119 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,9 +4,11 @@ use crate::{Font, Pixels}; use std::borrow::Cow; -/// The settings of an application. +/// The settings of an iced [`Program`]. +/// +/// [`Program`]: crate::Program #[derive(Debug, Clone)] -pub struct Settings<Flags> { +pub struct Settings<Flags = ()> { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or @@ -18,9 +20,9 @@ pub struct Settings<Flags> { /// They will be ignored on the Web. pub window: window::Settings, - /// The data needed to initialize the [`Application`]. + /// The data needed to initialize the [`Program`]. /// - /// [`Application`]: crate::Application + /// [`Program`]: crate::Program pub flags: Flags, /// The fonts to load on boot. @@ -49,9 +51,9 @@ pub struct Settings<Flags> { } impl<Flags> Settings<Flags> { - /// Initialize [`Application`] settings using the given data. + /// Initialize [`Program`] settings using the given data. /// - /// [`Application`]: crate::Application + /// [`Program`]: crate::Program pub fn with_flags(flags: Flags) -> Self { let default_settings = Settings::<()>::default(); diff --git a/src/time.rs b/src/time.rs index e255d7513c..26d31c0a4c 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,5 +1,5 @@ //! Listen and react to time. -pub use iced_core::time::{Duration, Instant}; +pub use crate::core::time::{Duration, Instant}; #[allow(unused_imports)] #[cfg_attr( diff --git a/style/Cargo.toml b/style/Cargo.toml deleted file mode 100644 index 3f00e787e7..0000000000 --- a/style/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "iced_style" -description = "The default set of styles of Iced" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -homepage.workspace = true -categories.workspace = true -keywords.workspace = true - -[dependencies] -iced_core.workspace = true -iced_core.features = ["palette"] - -palette.workspace = true -once_cell.workspace = true diff --git a/style/src/application.rs b/style/src/application.rs deleted file mode 100644 index e9a1f4ff45..0000000000 --- a/style/src/application.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Change the appearance of an application. -use iced_core::Color; - -/// A set of rules that dictate the style of an application. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Returns the [`Appearance`] of the application for the provided [`Style`]. - /// - /// [`Style`]: Self::Style - fn appearance(&self, style: &Self::Style) -> Appearance; -} - -/// The appearance of an application. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The background [`Color`] of the application. - pub background_color: Color, - - /// The default text [`Color`] of the application. - pub text_color: Color, -} diff --git a/style/src/button.rs b/style/src/button.rs deleted file mode 100644 index 0d7a668aca..0000000000 --- a/style/src/button.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Change the apperance of a button. -use iced_core::{Background, Border, Color, Shadow, Vector}; - -/// The appearance of a button. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The amount of offset to apply to the shadow of the button. - pub shadow_offset: Vector, - /// The [`Background`] of the button. - pub background: Option<Background>, - /// The text [`Color`] of the button. - pub text_color: Color, - /// The [`Border`] of the buton. - pub border: Border, - /// The [`Shadow`] of the butoon. - pub shadow: Shadow, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - shadow_offset: Vector::default(), - background: None, - text_color: Color::BLACK, - border: Border::default(), - shadow: Shadow::default(), - } - } -} - -/// A set of rules that dictate the style of a button. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a button. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the hovered [`Appearance`] of a button. - 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 - } - } - - /// Produces the pressed [`Appearance`] of a button. - fn pressed(&self, style: &Self::Style) -> Appearance { - Appearance { - shadow_offset: Vector::default(), - ..self.active(style) - } - } - - /// Produces the disabled [`Appearance`] of a button. - fn disabled(&self, style: &Self::Style) -> Appearance { - let active = self.active(style); - - Appearance { - shadow_offset: Vector::default(), - background: active.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.mul_alpha(0.5)) - } - }), - text_color: Color { - a: active.text_color.a * 0.5, - ..active.text_color - }, - ..active - } - } -} diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs deleted file mode 100644 index 82c1766fa9..0000000000 --- a/style/src/checkbox.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Change the appearance of a checkbox. -use iced_core::{Background, Border, Color}; - -/// The appearance of a checkbox. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the checkbox. - pub background: Background, - /// The icon [`Color`] of the checkbox. - pub icon_color: Color, - /// The [`Border`] of hte checkbox. - pub border: Border, - /// The text [`Color`] of the checkbox. - pub text_color: Option<Color>, -} - -/// A set of rules that dictate the style of a checkbox. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a checkbox. - fn active(&self, style: &Self::Style, is_checked: bool) -> Appearance; - - /// Produces the hovered [`Appearance`] of a checkbox. - fn hovered(&self, style: &Self::Style, is_checked: bool) -> Appearance; - - /// Produces the disabled [`Appearance`] of a checkbox. - fn disabled(&self, style: &Self::Style, is_checked: bool) -> Appearance; -} diff --git a/style/src/container.rs b/style/src/container.rs deleted file mode 100644 index 00649c25eb..0000000000 --- a/style/src/container.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Change the appearance of a container. -use crate::core::{Background, Border, Color, Pixels, Shadow}; - -/// The appearance of a container. -#[derive(Debug, Clone, Copy, Default)] -pub struct Appearance { - /// The text [`Color`] of the container. - pub text_color: Option<Color>, - /// The [`Background`] of the container. - pub background: Option<Background>, - /// The [`Border`] of the container. - pub border: Border, - /// The [`Shadow`] of the container. - pub shadow: Shadow, -} - -impl Appearance { - /// Derives a new [`Appearance`] with a border of the given [`Color`] and - /// `width`. - pub fn with_border( - self, - color: impl Into<Color>, - width: impl Into<Pixels>, - ) -> Self { - Self { - border: Border { - color: color.into(), - width: width.into().0, - ..Border::default() - }, - ..self - } - } - - /// Derives a new [`Appearance`] with the given [`Background`]. - pub fn with_background(self, background: impl Into<Background>) -> Self { - Self { - background: Some(background.into()), - ..self - } - } -} - -/// A set of rules that dictate the [`Appearance`] of a container. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// 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 deleted file mode 100644 index 3c2865ebeb..0000000000 --- a/style/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! The styling library of Iced. -//! -//! It contains a set of styles and stylesheets for most of the built-in -//! widgets. -//! -//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true) -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![forbid(unsafe_code, rust_2018_idioms)] -#![deny( - unused_results, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] -pub use iced_core as core; - -pub mod application; -pub mod button; -pub mod checkbox; -pub mod container; -pub mod menu; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod qr_code; -pub mod radio; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod svg; -pub mod text_editor; -pub mod text_input; -pub mod theme; -pub mod toggler; - -pub use theme::Theme; diff --git a/style/src/menu.rs b/style/src/menu.rs deleted file mode 100644 index be60a3f8f6..0000000000 --- a/style/src/menu.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Change the appearance of menus. -use iced_core::{Background, Border, Color}; - -/// The appearance of a menu. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The text [`Color`] of the menu. - pub text_color: Color, - /// The [`Background`] of the menu. - pub background: Background, - /// The [`Border`] of the menu. - pub border: Border, - /// The text [`Color`] of a selected option in the menu. - pub selected_text_color: Color, - /// The background [`Color`] of a selected option in the menu. - pub selected_background: Background, -} - -/// The style sheet of a menu. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; - - /// Produces the [`Appearance`] of a menu. - fn appearance(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs deleted file mode 100644 index 3557058435..0000000000 --- a/style/src/pane_grid.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Change the appearance of a pane grid. -use iced_core::{Background, Border, Color}; - -/// The appearance of the hovered region of a pane grid. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the pane region. - pub background: Background, - /// The [`Border`] of the pane region. - pub border: Border, -} - -/// A line. -/// -/// It is normally used to define the highlight of something, like a split. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Line { - /// The [`Color`] of the [`Line`]. - pub color: Color, - - /// The width of the [`Line`]. - pub width: f32, -} - -/// A set of rules that dictate the style of a container. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// The [`Appearance`] to draw when a pane is hovered. - fn hovered_region(&self, style: &Self::Style) -> Appearance; - - /// The [`Line`] to draw when a split is picked. - fn picked_split(&self, style: &Self::Style) -> Option<Line>; - - /// The [`Line`] to draw when a split is hovered. - fn hovered_split(&self, style: &Self::Style) -> Option<Line>; -} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs deleted file mode 100644 index 8f008f4a31..0000000000 --- a/style/src/pick_list.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Change the appearance of a pick list. -use iced_core::{Background, Border, Color}; - -/// The appearance of a pick list. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The text [`Color`] of the pick list. - pub text_color: Color, - /// The placeholder [`Color`] of the pick list. - pub placeholder_color: Color, - /// The handle [`Color`] of the pick list. - pub handle_color: Color, - /// The [`Background`] of the pick list. - pub background: Background, - /// The [`Border`] of the pick list. - pub border: Border, -} - -/// A set of rules that dictate the style of a container. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default + Clone; - - /// Produces the active [`Appearance`] of a pick list. - fn active(&self, style: &<Self as StyleSheet>::Style) -> Appearance; - - /// Produces the hovered [`Appearance`] of a pick list. - fn hovered(&self, style: &<Self as StyleSheet>::Style) -> Appearance; -} diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs deleted file mode 100644 index b62512d8a5..0000000000 --- a/style/src/progress_bar.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Change the appearance of a progress bar. -use crate::core::border; -use crate::core::Background; - -/// The appearance of a progress bar. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the progress bar. - pub background: Background, - /// The [`Background`] of the bar of the progress bar. - pub bar: Background, - /// The border radius of the progress bar. - pub border_radius: border::Radius, -} - -/// A set of rules that dictate the style of a progress bar. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the [`Appearance`] of the progress bar. - fn appearance(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/qr_code.rs b/style/src/qr_code.rs deleted file mode 100644 index 02c4709a5f..0000000000 --- a/style/src/qr_code.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Change the appearance of a QR code. -use crate::core::Color; - -/// The appearance of a QR code. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The color of the QR code data cells - pub cell: Color, - /// The color of the QR code background - pub background: Color, -} - -/// A set of rules that dictate the style of a QR code. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of a QR code. - fn appearance(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/radio.rs b/style/src/radio.rs deleted file mode 100644 index 06c49029a6..0000000000 --- a/style/src/radio.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Change the appearance of radio buttons. -use iced_core::{Background, Color}; - -/// The appearance of a radio button. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the radio button. - pub background: Background, - /// The [`Color`] of the dot of the radio button. - pub dot_color: Color, - /// The border width of the radio button. - pub border_width: f32, - /// The border [`Color`] of the radio button. - pub border_color: Color, - /// The text [`Color`] of the radio button. - pub text_color: Option<Color>, -} - -/// A set of rules that dictate the style of a radio button. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a radio button. - fn active(&self, style: &Self::Style, is_selected: bool) -> Appearance; - - /// Produces the hovered [`Appearance`] of a radio button. - fn hovered(&self, style: &Self::Style, is_selected: bool) -> Appearance; -} diff --git a/style/src/rule.rs b/style/src/rule.rs deleted file mode 100644 index 12980da7cb..0000000000 --- a/style/src/rule.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Change the appearance of a rule. -use crate::core::border; -use crate::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: border::Radius, - /// The [`FillMode`] of the rule. - pub fill_mode: FillMode, -} - -/// A set of rules that dictate the style of a rule. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of a rule. - fn appearance(&self, style: &Self::Style) -> Appearance; -} - -/// The fill mode of a rule. -#[derive(Debug, Clone, Copy)] -pub enum FillMode { - /// Fill the whole length of the container. - Full, - /// Fill a percent of the length of the container. The rule - /// will be centered in that container. - /// - /// The range is `[0.0, 100.0]`. - Percent(f32), - /// Uniform offset from each end, length units. - Padded(u16), - /// Different offset on each end of the rule, length units. - /// First = top or left. - AsymmetricPadding(u16, u16), -} - -impl FillMode { - /// Return the starting offset and length of the rule. - /// - /// * `space` - The space to fill. - /// - /// # Returns - /// - /// * (`starting_offset`, `length`) - pub fn fill(&self, space: f32) -> (f32, f32) { - match *self { - FillMode::Full => (0.0, space), - FillMode::Percent(percent) => { - if percent >= 100.0 { - (0.0, space) - } else { - let percent_width = (space * percent / 100.0).round(); - - (((space - percent_width) / 2.0).round(), percent_width) - } - } - FillMode::Padded(padding) => { - if padding == 0 { - (0.0, space) - } else { - let padding = padding as f32; - let mut line_width = space - (padding * 2.0); - if line_width < 0.0 { - line_width = 0.0; - } - - (padding, line_width) - } - } - FillMode::AsymmetricPadding(first_pad, second_pad) => { - let first_pad = first_pad as f32; - let second_pad = second_pad as f32; - let mut line_width = space - first_pad - second_pad; - if line_width < 0.0 { - line_width = 0.0; - } - - (first_pad, line_width) - } - } - } -} diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs deleted file mode 100644 index 6f37305f12..0000000000 --- a/style/src/scrollable.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Change the appearance of a scrollable. -use crate::core::{Background, Border, Color}; - -/// The appearance of a scrollable. -#[derive(Debug, Clone, Copy)] -pub struct Scrollbar { - /// The [`Background`] of a scrollable. - pub background: Option<Background>, - /// The [`Border`] of a scrollable. - pub border: Border, - /// The appearance of the [`Scroller`] of a scrollable. - pub scroller: Scroller, -} - -/// The appearance of the scroller of a scrollable. -#[derive(Debug, Clone, Copy)] -pub struct Scroller { - /// The [`Color`] of the scroller. - pub color: Color, - /// The [`Border`] of the scroller. - pub border: Border, -} - -/// A set of rules that dictate the style of a scrollable. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active scrollbar. - fn active(&self, style: &Self::Style) -> Scrollbar; - - /// Produces the style of a scrollbar when the scrollable is being hovered. - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> Scrollbar; - - /// Produces the style of a scrollbar that is being dragged. - fn dragging(&self, style: &Self::Style) -> Scrollbar { - self.hovered(style, true) - } - - /// Produces the style of an active horizontal scrollbar. - fn active_horizontal(&self, style: &Self::Style) -> Scrollbar { - self.active(style) - } - - /// Produces the style of a horizontal scrollbar when the scrollable is being hovered. - fn hovered_horizontal( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> Scrollbar { - self.hovered(style, is_mouse_over_scrollbar) - } - - /// Produces the style of a horizontal scrollbar that is being dragged. - fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar { - self.hovered_horizontal(style, true) - } -} diff --git a/style/src/slider.rs b/style/src/slider.rs deleted file mode 100644 index bf1c732961..0000000000 --- a/style/src/slider.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Change the apperance of a slider. -use crate::core::border; -use crate::core::Color; - -/// The appearance of a slider. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The colors of the rail of the slider. - pub rail: Rail, - /// The appearance of the [`Handle`] of the slider. - pub handle: Handle, -} - -/// The appearance of a slider rail -#[derive(Debug, Clone, Copy)] -pub struct Rail { - /// The colors of the rail of the slider. - pub colors: (Color, Color), - /// The width of the stroke of a slider rail. - pub width: f32, - /// The border radius of the corners of the rail. - pub border_radius: border::Radius, -} - -/// The appearance of the handle of a slider. -#[derive(Debug, Clone, Copy)] -pub struct Handle { - /// The shape of the handle. - pub shape: HandleShape, - /// The [`Color`] of the handle. - pub color: Color, - /// The border width of the handle. - pub border_width: f32, - /// The border [`Color`] of the handle. - pub border_color: Color, -} - -/// The shape of the handle of a slider. -#[derive(Debug, Clone, Copy)] -pub enum HandleShape { - /// A circular handle. - Circle { - /// The radius of the circle. - radius: f32, - }, - /// A rectangular shape. - Rectangle { - /// The width of the rectangle. - width: u16, - /// The border radius of the corners of the rectangle. - border_radius: border::Radius, - }, -} - -/// A set of rules that dictate the style of a slider. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active slider. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of an hovered slider. - fn hovered(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a slider that is being dragged. - fn dragging(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/svg.rs b/style/src/svg.rs deleted file mode 100644 index 5053f9f8cc..0000000000 --- a/style/src/svg.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Change the appearance of a svg. - -use iced_core::Color; - -/// The appearance of an SVG. -#[derive(Debug, Default, Clone, Copy)] -pub struct Appearance { - /// The [`Color`] filter of an SVG. - /// - /// Useful for coloring a symbolic icon. - /// - /// `None` keeps the original color. - pub color: Option<Color>, -} - -/// The stylesheet of a svg. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the [`Appearance`] of the svg. - fn appearance(&self, style: &Self::Style) -> Appearance; - - /// Produces the hovered [`Appearance`] of a svg content. - fn hovered(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs deleted file mode 100644 index 87f481e30b..0000000000 --- a/style/src/text_editor.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Change the appearance of a text editor. -use crate::core::{Background, Border, Color}; - -/// The appearance of a text input. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the text editor. - pub background: Background, - /// The [`Border`] of the text editor. - pub border: Border, -} - -/// A set of rules that dictate the style of a text input. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active text input. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a focused text input. - fn focused(&self, style: &Self::Style) -> Appearance; - - /// Produces the [`Color`] of the placeholder of a text input. - fn placeholder_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a text input. - fn value_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a disabled text input. - fn disabled_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the selection of a text input. - fn selection_color(&self, style: &Self::Style) -> Color; - - /// Produces the style of an hovered text input. - fn hovered(&self, style: &Self::Style) -> Appearance { - self.focused(style) - } - - /// Produces the style of a disabled text input. - fn disabled(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/text_input.rs b/style/src/text_input.rs deleted file mode 100644 index 8ba9957f51..0000000000 --- a/style/src/text_input.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Change the appearance of a text input. -use iced_core::{Background, Border, Color}; - -/// The appearance of a text input. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the text input. - pub background: Background, - /// The [`Border`] of the text input. - pub border: Border, - /// The icon [`Color`] of the text input. - pub icon_color: Color, -} - -/// A set of rules that dictate the style of a text input. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active text input. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a focused text input. - fn focused(&self, style: &Self::Style) -> Appearance; - - /// Produces the [`Color`] of the placeholder of a text input. - fn placeholder_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a text input. - fn value_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a disabled text input. - fn disabled_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the selection of a text input. - fn selection_color(&self, style: &Self::Style) -> Color; - - /// Produces the style of an hovered text input. - fn hovered(&self, style: &Self::Style) -> Appearance { - self.focused(style) - } - - /// Produces the style of a disabled text input. - fn disabled(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/theme.rs b/style/src/theme.rs deleted file mode 100644 index 5909498f0d..0000000000 --- a/style/src/theme.rs +++ /dev/null @@ -1,1544 +0,0 @@ -//! Use the built-in theme and styles. -pub mod palette; - -pub use palette::Palette; - -use crate::application; -use crate::button; -use crate::checkbox; -use crate::container; -use crate::core::widget::text; -use crate::menu; -use crate::pane_grid; -use crate::pick_list; -use crate::progress_bar; -use crate::qr_code; -use crate::radio; -use crate::rule; -use crate::scrollable; -use crate::slider; -use crate::svg; -use crate::text_editor; -use crate::text_input; -use crate::toggler; - -use crate::core::{Background, Border, Color, Shadow, Vector}; - -use std::fmt; -use std::rc::Rc; -use std::sync::Arc; - -/// A built-in theme. -#[derive(Debug, Clone, PartialEq, Default)] -pub enum Theme { - /// The built-in light variant. - #[default] - Light, - /// The built-in dark variant. - Dark, - /// The built-in Dracula variant. - Dracula, - /// The built-in Nord variant. - Nord, - /// The built-in Solarized Light variant. - SolarizedLight, - /// The built-in Solarized Dark variant. - SolarizedDark, - /// The built-in Gruvbox Light variant. - GruvboxLight, - /// The built-in Gruvbox Dark variant. - GruvboxDark, - /// The built-in Catppuccin Latte variant. - CatppuccinLatte, - /// The built-in Catppuccin Frappé variant. - CatppuccinFrappe, - /// The built-in Catppuccin Macchiato variant. - CatppuccinMacchiato, - /// The built-in Catppuccin Mocha variant. - CatppuccinMocha, - /// The built-in Tokyo Night variant. - TokyoNight, - /// The built-in Tokyo Night Storm variant. - TokyoNightStorm, - /// The built-in Tokyo Night Light variant. - TokyoNightLight, - /// The built-in Kanagawa Wave variant. - KanagawaWave, - /// The built-in Kanagawa Dragon variant. - KanagawaDragon, - /// The built-in Kanagawa Lotus variant. - KanagawaLotus, - /// The built-in Moonfly variant. - Moonfly, - /// The built-in Nightfly variant. - Nightfly, - /// The built-in Oxocarbon variant. - Oxocarbon, - /// A [`Theme`] that uses a [`Custom`] palette. - Custom(Arc<Custom>), -} - -impl Theme { - /// A list with all the defined themes. - pub const ALL: &'static [Self] = &[ - Self::Light, - Self::Dark, - Self::Dracula, - Self::Nord, - Self::SolarizedLight, - Self::SolarizedDark, - Self::GruvboxLight, - Self::GruvboxDark, - Self::CatppuccinLatte, - Self::CatppuccinFrappe, - Self::CatppuccinMacchiato, - Self::CatppuccinMocha, - Self::TokyoNight, - Self::TokyoNightStorm, - Self::TokyoNightLight, - Self::KanagawaWave, - Self::KanagawaDragon, - Self::KanagawaLotus, - Self::Moonfly, - Self::Nightfly, - Self::Oxocarbon, - ]; - - /// Creates a new custom [`Theme`] from the given [`Palette`]. - pub fn custom(name: String, palette: Palette) -> Self { - Self::custom_with_fn(name, palette, palette::Extended::generate) - } - - /// Creates a new custom [`Theme`] from the given [`Palette`], with - /// a custom generator of a [`palette::Extended`]. - pub fn custom_with_fn( - name: String, - palette: Palette, - generate: impl FnOnce(Palette) -> palette::Extended, - ) -> Self { - Self::Custom(Arc::new(Custom::with_fn(name, palette, generate))) - } - - /// Returns the [`Palette`] of the [`Theme`]. - pub fn palette(&self) -> Palette { - match self { - Self::Light => Palette::LIGHT, - Self::Dark => Palette::DARK, - Self::Dracula => Palette::DRACULA, - Self::Nord => Palette::NORD, - Self::SolarizedLight => Palette::SOLARIZED_LIGHT, - Self::SolarizedDark => Palette::SOLARIZED_DARK, - Self::GruvboxLight => Palette::GRUVBOX_LIGHT, - Self::GruvboxDark => Palette::GRUVBOX_DARK, - Self::CatppuccinLatte => Palette::CATPPUCCIN_LATTE, - Self::CatppuccinFrappe => Palette::CATPPUCCIN_FRAPPE, - Self::CatppuccinMacchiato => Palette::CATPPUCCIN_MACCHIATO, - Self::CatppuccinMocha => Palette::CATPPUCCIN_MOCHA, - Self::TokyoNight => Palette::TOKYO_NIGHT, - Self::TokyoNightStorm => Palette::TOKYO_NIGHT_STORM, - Self::TokyoNightLight => Palette::TOKYO_NIGHT_LIGHT, - Self::KanagawaWave => Palette::KANAGAWA_WAVE, - Self::KanagawaDragon => Palette::KANAGAWA_DRAGON, - Self::KanagawaLotus => Palette::KANAGAWA_LOTUS, - Self::Moonfly => Palette::MOONFLY, - Self::Nightfly => Palette::NIGHTFLY, - Self::Oxocarbon => Palette::OXOCARBON, - Self::Custom(custom) => custom.palette, - } - } - - /// Returns the [`palette::Extended`] of the [`Theme`]. - pub fn extended_palette(&self) -> &palette::Extended { - match self { - Self::Light => &palette::EXTENDED_LIGHT, - Self::Dark => &palette::EXTENDED_DARK, - Self::Dracula => &palette::EXTENDED_DRACULA, - Self::Nord => &palette::EXTENDED_NORD, - Self::SolarizedLight => &palette::EXTENDED_SOLARIZED_LIGHT, - Self::SolarizedDark => &palette::EXTENDED_SOLARIZED_DARK, - Self::GruvboxLight => &palette::EXTENDED_GRUVBOX_LIGHT, - Self::GruvboxDark => &palette::EXTENDED_GRUVBOX_DARK, - Self::CatppuccinLatte => &palette::EXTENDED_CATPPUCCIN_LATTE, - Self::CatppuccinFrappe => &palette::EXTENDED_CATPPUCCIN_FRAPPE, - Self::CatppuccinMacchiato => { - &palette::EXTENDED_CATPPUCCIN_MACCHIATO - } - Self::CatppuccinMocha => &palette::EXTENDED_CATPPUCCIN_MOCHA, - Self::TokyoNight => &palette::EXTENDED_TOKYO_NIGHT, - Self::TokyoNightStorm => &palette::EXTENDED_TOKYO_NIGHT_STORM, - Self::TokyoNightLight => &palette::EXTENDED_TOKYO_NIGHT_LIGHT, - Self::KanagawaWave => &palette::EXTENDED_KANAGAWA_WAVE, - Self::KanagawaDragon => &palette::EXTENDED_KANAGAWA_DRAGON, - Self::KanagawaLotus => &palette::EXTENDED_KANAGAWA_LOTUS, - Self::Moonfly => &palette::EXTENDED_MOONFLY, - Self::Nightfly => &palette::EXTENDED_NIGHTFLY, - Self::Oxocarbon => &palette::EXTENDED_OXOCARBON, - Self::Custom(custom) => &custom.extended, - } - } -} - -impl fmt::Display for Theme { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Light => write!(f, "Light"), - Self::Dark => write!(f, "Dark"), - Self::Dracula => write!(f, "Dracula"), - Self::Nord => write!(f, "Nord"), - Self::SolarizedLight => write!(f, "Solarized Light"), - Self::SolarizedDark => write!(f, "Solarized Dark"), - Self::GruvboxLight => write!(f, "Gruvbox Light"), - Self::GruvboxDark => write!(f, "Gruvbox Dark"), - Self::CatppuccinLatte => write!(f, "Catppuccin Latte"), - Self::CatppuccinFrappe => write!(f, "Catppuccin Frappé"), - Self::CatppuccinMacchiato => write!(f, "Catppuccin Macchiato"), - Self::CatppuccinMocha => write!(f, "Catppuccin Mocha"), - Self::TokyoNight => write!(f, "Tokyo Night"), - Self::TokyoNightStorm => write!(f, "Tokyo Night Storm"), - Self::TokyoNightLight => write!(f, "Tokyo Night Light"), - Self::KanagawaWave => write!(f, "Kanagawa Wave"), - Self::KanagawaDragon => write!(f, "Kanagawa Dragon"), - Self::KanagawaLotus => write!(f, "Kanagawa Lotus"), - Self::Moonfly => write!(f, "Moonfly"), - Self::Nightfly => write!(f, "Nightfly"), - Self::Oxocarbon => write!(f, "Oxocarbon"), - Self::Custom(custom) => custom.fmt(f), - } - } -} - -/// A [`Theme`] with a customized [`Palette`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Custom { - name: String, - palette: Palette, - extended: palette::Extended, -} - -impl Custom { - /// Creates a [`Custom`] theme from the given [`Palette`]. - pub fn new(name: String, palette: Palette) -> Self { - Self::with_fn(name, palette, palette::Extended::generate) - } - - /// Creates a [`Custom`] theme from the given [`Palette`] with - /// a custom generator of a [`palette::Extended`]. - pub fn with_fn( - name: String, - palette: Palette, - generate: impl FnOnce(Palette) -> palette::Extended, - ) -> Self { - Self { - name, - palette, - extended: generate(palette), - } - } -} - -impl fmt::Display for Custom { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -/// The style of an application. -#[derive(Default)] -pub enum Application { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn application::StyleSheet<Style = Theme>>), -} - -impl Application { - /// Creates a custom [`Application`] style. - pub fn custom( - custom: impl application::StyleSheet<Style = Theme> + 'static, - ) -> Self { - Self::Custom(Box::new(custom)) - } -} - -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(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> application::Appearance> application::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> application::Appearance { - (self)(style) - } -} - -/// The style of a button. -#[derive(Default)] -pub enum Button { - /// The primary style. - #[default] - Primary, - /// The secondary style. - Secondary, - /// The positive style. - Positive, - /// The destructive style. - Destructive, - /// The text style. - /// - /// Useful for links! - Text, - /// A custom style. - Custom(Box<dyn button::StyleSheet<Style = Theme>>), -} - -impl Button { - /// Creates a custom [`Button`] style variant. - pub fn custom( - style_sheet: impl button::StyleSheet<Style = Theme> + 'static, - ) -> Self { - Self::Custom(Box::new(style_sheet)) - } -} - -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: Border::with_radius(2), - ..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 - }, - Button::Custom(custom) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> button::Appearance { - let palette = self.extended_palette(); - - if let Button::Custom(custom) = style { - return custom.hovered(self); - } - - let active = self.active(style); - - 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 | Button::Custom(_) => None, - }; - - button::Appearance { - background: background.map(Background::from), - ..active - } - } - - fn pressed(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom(custom) = style { - return custom.pressed(self); - } - - button::Appearance { - shadow_offset: Vector::default(), - ..self.active(style) - } - } - - fn disabled(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom(custom) = style { - return custom.disabled(self); - } - - let active = self.active(style); - - button::Appearance { - shadow_offset: Vector::default(), - background: active.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.mul_alpha(0.5)) - } - }), - text_color: Color { - a: active.text_color.a * 0.5, - ..active.text_color - }, - ..active - } - } -} - -/// The style of a checkbox. -#[derive(Default)] -pub enum Checkbox { - /// The primary style. - #[default] - Primary, - /// The secondary style. - Secondary, - /// The success style. - Success, - /// The danger style. - Danger, - /// A custom style. - Custom(Box<dyn checkbox::StyleSheet<Style = Theme>>), -} - -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.strong, - 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, - ), - Checkbox::Custom(custom) => custom.active(self, 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.strong, - 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, - ), - Checkbox::Custom(custom) => custom.hovered(self, is_checked), - } - } - - fn disabled( - &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.background.strong, - is_checked, - ), - Checkbox::Secondary => checkbox_appearance( - palette.background.strong.color, - palette.background.weak, - palette.background.weak, - is_checked, - ), - Checkbox::Success => checkbox_appearance( - palette.success.base.text, - palette.background.weak, - palette.success.weak, - is_checked, - ), - Checkbox::Danger => checkbox_appearance( - palette.danger.base.text, - palette.background.weak, - palette.danger.weak, - is_checked, - ), - Checkbox::Custom(custom) => custom.active(self, is_checked), - } - } -} - -fn checkbox_appearance( - icon_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 - }), - icon_color, - border: Border { - radius: 2.0.into(), - width: 1.0, - color: accent.color, - }, - text_color: None, - } -} - -/// The style of a container. -#[derive(Default)] -pub enum Container { - /// No style. - #[default] - Transparent, - /// A simple box. - Box, - /// A custom style. - Custom(Box<dyn container::StyleSheet<Style = Theme>>), -} - -impl From<container::Appearance> for Container { - fn from(appearance: container::Appearance) -> Self { - Self::Custom(Box::new(move |_: &_| appearance)) - } -} - -impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl container::StyleSheet for Theme { - type Style = Container; - - fn appearance(&self, style: &Self::Style) -> container::Appearance { - match style { - Container::Transparent => container::Appearance::default(), - Container::Box => { - let palette = self.extended_palette(); - - container::Appearance { - text_color: None, - background: Some(palette.background.weak.color.into()), - border: Border::with_radius(2), - shadow: Shadow::default(), - } - } - Container::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> container::Appearance> container::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> container::Appearance { - (self)(style) - } -} - -/// The style of a slider. -#[derive(Default)] -pub enum Slider { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn slider::StyleSheet<Style = Theme>>), -} - -impl slider::StyleSheet for Theme { - type Style = Slider; - - fn active(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let palette = self.extended_palette(); - - let handle = slider::Handle { - shape: slider::HandleShape::Rectangle { - width: 8, - border_radius: 4.0.into(), - }, - color: Color::WHITE, - border_color: Color::WHITE, - border_width: 1.0, - }; - - slider::Appearance { - rail: slider::Rail { - colors: ( - palette.primary.base.color, - palette.secondary.base.color, - ), - width: 4.0, - border_radius: 2.0.into(), - }, - handle: slider::Handle { - color: palette.background.base.color, - border_color: palette.primary.base.color, - ..handle - }, - } - } - Slider::Custom(custom) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.weak.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.hovered(self), - } - } - - fn dragging(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.base.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.dragging(self), - } - } -} - -/// The style of a menu. -#[derive(Clone, Default)] -pub enum Menu { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Rc<dyn menu::StyleSheet<Style = Theme>>), -} - -impl menu::StyleSheet for Theme { - type Style = Menu; - - fn appearance(&self, style: &Self::Style) -> menu::Appearance { - match style { - Menu::Default => { - let palette = self.extended_palette(); - - menu::Appearance { - text_color: palette.background.weak.text, - background: palette.background.weak.color.into(), - border: Border { - width: 1.0, - radius: 0.0.into(), - color: palette.background.strong.color, - }, - selected_text_color: palette.primary.strong.text, - selected_background: palette.primary.strong.color.into(), - } - } - Menu::Custom(custom) => custom.appearance(self), - } - } -} - -impl From<PickList> for Menu { - fn from(pick_list: PickList) -> Self { - match pick_list { - PickList::Default => Self::Default, - PickList::Custom(_, menu) => Self::Custom(menu), - } - } -} - -/// The style of a pick list. -#[derive(Clone, Default)] -pub enum PickList { - /// The default style. - #[default] - Default, - /// A custom style. - Custom( - Rc<dyn pick_list::StyleSheet<Style = Theme>>, - Rc<dyn menu::StyleSheet<Style = Theme>>, - ), -} - -impl pick_list::StyleSheet for Theme { - type Style = PickList; - - fn active(&self, style: &Self::Style) -> pick_list::Appearance { - match style { - PickList::Default => { - 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, - handle_color: palette.background.weak.text, - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - PickList::Custom(custom, _) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> pick_list::Appearance { - match style { - PickList::Default => { - 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, - handle_color: palette.background.weak.text, - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - } - } - PickList::Custom(custom, _) => custom.hovered(self), - } - } -} - -/// The style of a radio button. -#[derive(Default)] -pub enum Radio { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn radio::StyleSheet<Style = Theme>>), -} - -impl radio::StyleSheet for Theme { - type Style = Radio; - - fn active( - &self, - style: &Self::Style, - is_selected: bool, - ) -> radio::Appearance { - match style { - Radio::Default => { - 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, - } - } - Radio::Custom(custom) => custom.active(self, is_selected), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_selected: bool, - ) -> radio::Appearance { - match style { - Radio::Default => { - let active = self.active(style, is_selected); - let palette = self.extended_palette(); - - radio::Appearance { - dot_color: palette.primary.strong.color, - background: palette.primary.weak.color.into(), - ..active - } - } - Radio::Custom(custom) => custom.hovered(self, is_selected), - } - } -} - -/// The style of a toggler. -#[derive(Default)] -pub enum Toggler { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn toggler::StyleSheet<Style = Theme>>), -} - -impl toggler::StyleSheet for Theme { - type Style = Toggler; - - fn active( - &self, - style: &Self::Style, - is_active: bool, - ) -> toggler::Appearance { - match style { - Toggler::Default => { - let palette = self.extended_palette(); - - toggler::Appearance { - background: if is_active { - palette.primary.strong.color - } else { - palette.background.strong.color - }, - background_border_width: 0.0, - background_border_color: Color::TRANSPARENT, - foreground: if is_active { - palette.primary.strong.text - } else { - palette.background.base.color - }, - foreground_border_width: 0.0, - foreground_border_color: Color::TRANSPARENT, - } - } - Toggler::Custom(custom) => custom.active(self, is_active), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_active: bool, - ) -> toggler::Appearance { - match style { - Toggler::Default => { - 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) - } - } - Toggler::Custom(custom) => custom.hovered(self, is_active), - } - } -} - -/// The style of a pane grid. -#[derive(Default)] -pub enum PaneGrid { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn pane_grid::StyleSheet<Style = Theme>>), -} - -impl pane_grid::StyleSheet for Theme { - type Style = PaneGrid; - - fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - pane_grid::Appearance { - background: Background::Color(Color { - a: 0.5, - ..palette.primary.base.color - }), - border: Border { - width: 2.0, - color: palette.primary.strong.color, - radius: 0.0.into(), - }, - } - } - PaneGrid::Custom(custom) => custom.hovered_region(self), - } - } - - fn picked_split(&self, style: &Self::Style) -> Option<pane_grid::Line> { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - Some(pane_grid::Line { - color: palette.primary.strong.color, - width: 2.0, - }) - } - PaneGrid::Custom(custom) => custom.picked_split(self), - } - } - - fn hovered_split(&self, style: &Self::Style) -> Option<pane_grid::Line> { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - Some(pane_grid::Line { - color: palette.primary.base.color, - width: 2.0, - }) - } - PaneGrid::Custom(custom) => custom.hovered_split(self), - } - } -} - -/// The style of a progress bar. -#[derive(Default)] -pub enum ProgressBar { - /// The primary style. - #[default] - Primary, - /// The success style. - Success, - /// The danger style. - Danger, - /// A custom style. - Custom(Box<dyn progress_bar::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> progress_bar::Appearance + 'static> From<T> - for ProgressBar -{ - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl progress_bar::StyleSheet for Theme { - type Style = ProgressBar; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - if let ProgressBar::Custom(custom) = style { - return custom.appearance(self); - } - - 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.into(), - }; - - 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(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> progress_bar::Appearance> progress_bar::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - (self)(style) - } -} - -/// The style of a QR Code. -#[derive(Default)] -pub enum QRCode { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn qr_code::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> qr_code::Appearance + 'static> From<T> for QRCode { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl qr_code::StyleSheet for Theme { - type Style = QRCode; - - fn appearance(&self, style: &Self::Style) -> qr_code::Appearance { - let palette = self.palette(); - - match style { - QRCode::Default => qr_code::Appearance { - cell: palette.text, - background: palette.background, - }, - QRCode::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> qr_code::Appearance> qr_code::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> qr_code::Appearance { - (self)(style) - } -} - -/// The style of a rule. -#[derive(Default)] -pub enum Rule { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn rule::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> rule::Appearance + 'static> From<T> for Rule { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl rule::StyleSheet for Theme { - type Style = Rule; - - fn appearance(&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.into(), - fill_mode: rule::FillMode::Full, - }, - Rule::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> rule::Appearance> rule::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> rule::Appearance { - (self)(style) - } -} - -/** - * Svg - */ -#[derive(Default)] -pub enum Svg { - /// No filtering to the rendered SVG. - #[default] - Default, - /// A custom style. - Custom(Box<dyn svg::StyleSheet<Style = Theme>>), -} - -impl Svg { - /// Creates a custom [`Svg`] style. - pub fn custom_fn(f: fn(&Theme) -> svg::Appearance) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl svg::StyleSheet for Theme { - type Style = Svg; - - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - match style { - Svg::Default => svg::Appearance::default(), - Svg::Custom(custom) => custom.appearance(self), - } - } - - fn hovered(&self, style: &Self::Style) -> svg::Appearance { - self.appearance(style) - } -} - -impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - (self)(style) - } - - fn hovered(&self, style: &Self::Style) -> svg::Appearance { - self.appearance(style) - } -} - -/// The style of a scrollable. -#[derive(Default)] -pub enum Scrollable { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn scrollable::StyleSheet<Style = Theme>>), -} - -impl Scrollable { - /// Creates a custom [`Scrollable`] theme. - pub fn custom<T: scrollable::StyleSheet<Style = Theme> + 'static>( - style: T, - ) -> Self { - Self::Custom(Box::new(style)) - } -} - -impl scrollable::StyleSheet for Theme { - type Style = Scrollable; - - fn active(&self, style: &Self::Style) -> scrollable::Scrollbar { - match style { - Scrollable::Default => { - let palette = self.extended_palette(); - - scrollable::Scrollbar { - background: Some(palette.background.weak.color.into()), - border: Border::with_radius(2), - scroller: scrollable::Scroller { - color: palette.background.strong.color, - border: Border::with_radius(2), - }, - } - } - Scrollable::Custom(custom) => custom.active(self), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> scrollable::Scrollbar { - match style { - Scrollable::Default => { - if is_mouse_over_scrollbar { - let palette = self.extended_palette(); - - scrollable::Scrollbar { - background: Some(palette.background.weak.color.into()), - border: Border::with_radius(2), - scroller: scrollable::Scroller { - color: palette.primary.strong.color, - border: Border::with_radius(2), - }, - } - } else { - self.active(style) - } - } - Scrollable::Custom(custom) => { - custom.hovered(self, is_mouse_over_scrollbar) - } - } - } - - fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar { - match style { - Scrollable::Default => self.hovered(style, true), - Scrollable::Custom(custom) => custom.dragging(self), - } - } - - fn active_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar { - match style { - Scrollable::Default => self.active(style), - Scrollable::Custom(custom) => custom.active_horizontal(self), - } - } - - fn hovered_horizontal( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> scrollable::Scrollbar { - match style { - Scrollable::Default => self.hovered(style, is_mouse_over_scrollbar), - Scrollable::Custom(custom) => { - custom.hovered_horizontal(self, is_mouse_over_scrollbar) - } - } - } - - fn dragging_horizontal( - &self, - style: &Self::Style, - ) -> scrollable::Scrollbar { - match style { - Scrollable::Default => self.hovered_horizontal(style, true), - Scrollable::Custom(custom) => custom.dragging_horizontal(self), - } - } -} - -/// The style of text. -#[derive(Clone, Copy, Default)] -pub enum Text { - /// The default style. - #[default] - Default, - /// Colored text. - Color(Color), -} - -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 => text::Appearance::default(), - Text::Color(c) => text::Appearance { color: Some(c) }, - } - } -} - -/// The style of a text input. -#[derive(Default)] -pub enum TextInput { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn text_input::StyleSheet<Style = Theme>>), -} - -impl text_input::StyleSheet for Theme { - type Style = TextInput; - - fn active(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.active(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn hovered(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.hovered(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.base.text, - }, - icon_color: palette.background.weak.text, - } - } - - fn focused(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.focused(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn placeholder_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.placeholder_color(self); - } - - let palette = self.extended_palette(); - - palette.background.strong.color - } - - fn value_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.value_color(self); - } - - let palette = self.extended_palette(); - - palette.background.base.text - } - - fn selection_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.selection_color(self); - } - - let palette = self.extended_palette(); - - palette.primary.weak.color - } - - fn disabled(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.disabled(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.weak.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.strong.color, - } - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.disabled_color(self); - } - - self.placeholder_color(style) - } -} - -/// The style of a text input. -#[derive(Default)] -pub enum TextEditor { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>), -} - -impl text_editor::StyleSheet for Theme { - type Style = TextEditor; - - fn active(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.active(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - - fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.hovered(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.base.text, - }, - } - } - - fn focused(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.focused(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - } - } - - fn placeholder_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.placeholder_color(self); - } - - let palette = self.extended_palette(); - - palette.background.strong.color - } - - fn value_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.value_color(self); - } - - let palette = self.extended_palette(); - - palette.background.base.text - } - - fn selection_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.selection_color(self); - } - - let palette = self.extended_palette(); - - palette.primary.weak.color - } - - fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.disabled(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.weak.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.disabled_color(self); - } - - self.placeholder_color(style) - } -} diff --git a/style/src/toggler.rs b/style/src/toggler.rs deleted file mode 100644 index 731e87ceb8..0000000000 --- a/style/src/toggler.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Change the appearance of a toggler. -use iced_core::Color; - -/// The appearance of a toggler. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The background [`Color`] of the toggler. - pub background: Color, - /// The width of the background border of the toggler. - pub background_border_width: f32, - /// The [`Color`] of the background border of the toggler. - pub background_border_color: Color, - /// The foreground [`Color`] of the toggler. - pub foreground: Color, - /// The width of the foreground border of the toggler. - pub foreground_border_width: f32, - /// The [`Color`] of the foreground border of the toggler. - pub foreground_border_color: Color, -} - -/// A set of rules that dictate the style of a toggler. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Returns the active [`Appearance`] of the toggler for the provided [`Style`]. - /// - /// [`Style`]: Self::Style - fn active(&self, style: &Self::Style, is_active: bool) -> Appearance; - - /// Returns the hovered [`Appearance`] of the toggler for the provided [`Style`]. - /// - /// [`Style`]: Self::Style - fn hovered(&self, style: &Self::Style, is_active: bool) -> Appearance; -} diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 5d3a7a6f06..b6487b3886 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -674,8 +674,8 @@ impl Backend { let physical_bounds = (Rectangle { x: bounds.x(), y: bounds.y(), - width: bounds.width(), - height: bounds.height(), + width: bounds.width().max(1.0), + height: bounds.height().max(1.0), } * transformation) * scale_factor; diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index f751873170..16787f89ae 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,5 +1,7 @@ use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Rectangle, Size, Transformation, Vector}; +use crate::core::{ + Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, +}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; @@ -192,10 +194,10 @@ impl Frame { self.transform.pre_translate(translation.x, translation.y); } - pub fn rotate(&mut self, angle: f32) { - self.transform = self - .transform - .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); + pub fn rotate(&mut self, angle: impl Into<Radians>) { + self.transform = self.transform.pre_concat( + tiny_skia::Transform::from_rotate(angle.into().0.to_degrees()), + ); } pub fn scale(&mut self, scale: impl Into<f32>) { diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 8f36f95586..d28cc4835f 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -238,6 +238,12 @@ fn draw( ) .expect("Create glyph pixel map"); + let opacity = color.a + * glyph + .color_opt + .map(|c| c.a() as f32 / 255.0) + .unwrap_or(1.0); + pixels.draw_pixmap( physical_glyph.x + placement.left, physical_glyph.y - placement.top @@ -246,7 +252,10 @@ fn draw( * transformation.scale_factor()) .round() as i32, pixmap, - &tiny_skia::PixmapPaint::default(), + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, tiny_skia::Transform::identity(), clip_mask, ); diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 21ccf6200e..a98825f172 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -5,6 +5,7 @@ use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; use std::collections::VecDeque; +use std::future::{self, Future}; use std::num::NonZeroU32; pub struct Compositor { @@ -31,8 +32,8 @@ impl crate::graphics::Compositor for Compositor { fn new<W: compositor::Window>( settings: Self::Settings, compatible_window: W, - ) -> Result<Self, Error> { - Ok(new(settings, compatible_window)) + ) -> impl Future<Output = Result<Self, Error>> { + future::ready(Ok(new(settings, compatible_window))) } fn create_renderer(&self) -> Self::Renderer { diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 77b6fa832e..09ddbe4d8b 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -27,7 +27,6 @@ pub struct Backend { text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, pipeline_storage: pipeline::Storage, - #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, } @@ -35,6 +34,7 @@ pub struct Backend { impl Backend { /// Creates a new [`Backend`]. pub fn new( + _adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, settings: Settings, @@ -46,7 +46,11 @@ impl Backend { triangle::Pipeline::new(device, format, settings.antialiasing); #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = image::Pipeline::new(device, format); + let image_pipeline = { + let backend = _adapter.get_info().backend; + + image::Pipeline::new(device, format, backend) + }; Self { quad_pipeline, diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 4598b0a687..890f3f8961 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -158,10 +158,3 @@ pub fn convert( texture } - -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -struct Vertex { - ndc: [f32; 2], - uv: [f32; 2], -} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 8cfcfff010..f4e0fbda40 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,6 +1,8 @@ //! Build and draw geometry. use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Rectangle, Size, Transformation, Vector}; +use crate::core::{ + Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, +}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -475,12 +477,12 @@ impl Frame { /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] - pub fn rotate(&mut self, angle: f32) { + pub fn rotate(&mut self, angle: impl Into<Radians>) { self.transforms.current.0 = self .transforms .current .0 - .pre_rotate(lyon::math::Angle::radians(angle)); + .pre_rotate(lyon::math::Angle::radians(angle.into().0)); } /// Applies a uniform scaling to the current transform of the [`Frame`]. diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 06c22870bd..c8e4a4c282 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -176,7 +176,11 @@ impl Data { } impl Pipeline { - pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + backend: wgpu::Backend, + ) -> Self { let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -318,7 +322,7 @@ impl Pipeline { multiview: None, }); - let texture_atlas = Atlas::new(device); + let texture_atlas = Atlas::new(device, backend); let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image texture atlas bind group"), diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 789e35b4d7..ea36e06d32 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -23,11 +23,19 @@ pub struct Atlas { } impl Atlas { - pub fn new(device: &wgpu::Device) -> Self { + pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + let layers = match backend { + // On the GL backend we start with 2 layers, to help wgpu figure + // out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D` + // https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371 + wgpu::Backend::Gl => vec![Layer::Empty, Layer::Empty], + _ => vec![Layer::Empty], + }; + let extent = wgpu::Extent3d { width: SIZE, height: SIZE, - depth_or_array_layers: 1, + depth_or_array_layers: layers.len() as u32, }; let texture = device.create_texture(&wgpu::TextureDescriptor { @@ -55,7 +63,7 @@ impl Atlas { Atlas { texture, texture_view, - layers: vec![Layer::Empty], + layers, } } diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 60b170cc73..560fcad22f 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -1,4 +1,3 @@ -use crate::graphics::color; use crate::graphics::gradient; use crate::quad::{self, Quad}; use crate::Buffer; @@ -59,106 +58,128 @@ impl Layer { #[derive(Debug)] pub struct Pipeline { + #[cfg(not(target_arch = "wasm32"))] pipeline: wgpu::RenderPipeline, } impl Pipeline { + #[allow(unused_variables)] pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, constants_layout: &wgpu::BindGroupLayout, ) -> Self { - let layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("iced_wgpu.quad.gradient.pipeline"), - push_constant_ranges: &[], - bind_group_layouts: &[constants_layout], - }); - - let shader = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu.quad.gradient.shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - if color::GAMMA_CORRECTION { - concat!( - include_str!("../shader/quad.wgsl"), - "\n", - include_str!("../shader/vertex.wgsl"), - "\n", - include_str!("../shader/quad/gradient.wgsl"), - "\n", - include_str!("../shader/color/oklab.wgsl") - ) - } else { - concat!( - include_str!("../shader/quad.wgsl"), - "\n", - include_str!("../shader/vertex.wgsl"), - "\n", - include_str!("../shader/quad/gradient.wgsl"), - "\n", - include_str!("../shader/color/linear_rgb.wgsl") - ) - }, - )), - }); - - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu.quad.gradient.pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "gradient_vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<Gradient>() as u64, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &wgpu::vertex_attr_array!( - // Colors 1-2 - 0 => Uint32x4, - // Colors 3-4 - 1 => Uint32x4, - // Colors 5-6 - 2 => Uint32x4, - // Colors 7-8 - 3 => Uint32x4, - // Offsets 1-8 - 4 => Uint32x4, - // Direction - 5 => Float32x4, - // Position & Scale - 6 => Float32x4, - // Border color - 7 => Float32x4, - // Border radius - 8 => Float32x4, - // Border width - 9 => Float32 - ), - }], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "gradient_fs_main", - targets: &quad::color_target_state(format), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Cw, - ..Default::default() + #[cfg(not(target_arch = "wasm32"))] + { + use crate::graphics::color; + + let layout = device.create_pipeline_layout( + &wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + push_constant_ranges: &[], + bind_group_layouts: &[constants_layout], }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, + ); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.gradient.shader"), + source: wgpu::ShaderSource::Wgsl( + std::borrow::Cow::Borrowed( + if color::GAMMA_CORRECTION { + concat!( + include_str!("../shader/quad.wgsl"), + "\n", + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!( + "../shader/quad/gradient.wgsl" + ), + "\n", + include_str!("../shader/color/oklab.wgsl") + ) + } else { + concat!( + include_str!("../shader/quad.wgsl"), + "\n", + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!( + "../shader/quad/gradient.wgsl" + ), + "\n", + include_str!( + "../shader/color/linear_rgb.wgsl" + ) + ) + }, + ), + ), + }); + + let pipeline = device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "gradient_vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Gradient>() + as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Colors 1-2 + 0 => Uint32x4, + // Colors 3-4 + 1 => Uint32x4, + // Colors 5-6 + 2 => Uint32x4, + // Colors 7-8 + 3 => Uint32x4, + // Offsets 1-8 + 4 => Uint32x4, + // Direction + 5 => Float32x4, + // Position & Scale + 6 => Float32x4, + // Border color + 7 => Float32x4, + // Border radius + 8 => Float32x4, + // Border width + 9 => Float32 + ), + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "gradient_fs_main", + targets: &quad::color_target_state(format), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, }, - multiview: None, - }); + ); - Self { pipeline } + Self { pipeline } + } + + #[cfg(target_arch = "wasm32")] + Self {} } + #[allow(unused_variables)] pub fn render<'a>( &'a self, render_pass: &mut wgpu::RenderPass<'a>, @@ -169,10 +190,13 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered(); - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, constants, &[]); - render_pass.set_vertex_buffer(0, layer.instances.slice(..)); + #[cfg(not(target_arch = "wasm32"))] + { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(0, layer.instances.slice(..)); - render_pass.draw(0..6, range.start as u32..range.end as u32); + render_pass.draw(0..6, range.start as u32..range.end as u32); + } } } diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index 4de7336267..a367d5e68e 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -22,7 +22,7 @@ fn rounded_box_sdf(to_center: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 { return length(max(abs(to_center) - size + vec2<f32>(radius, radius), vec2<f32>(0.0, 0.0))) - radius; } -// Based on the fragement position and the center of the quad, select one of the 4 radi. +// Based on the fragment position and the center of the quad, select one of the 4 radi. // Order matches CSS border radius attribute: // radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 331330164d..fa6b9373df 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,6 +6,8 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; +use std::future::Future; + /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { @@ -150,23 +152,25 @@ impl Compositor { /// Creates a new rendering [`Backend`] for this [`Compositor`]. pub fn create_backend(&self) -> Backend { - Backend::new(&self.device, &self.queue, self.settings, self.format) + Backend::new( + &self.adapter, + &self.device, + &self.queue, + self.settings, + self.format, + ) } } /// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and /// window. -pub fn new<W: compositor::Window>( +pub async fn new<W: compositor::Window>( settings: Settings, compatible_window: W, ) -> Result<Compositor, Error> { - let compositor = futures::executor::block_on(Compositor::request( - settings, - Some(compatible_window), - )) - .ok_or(Error::GraphicsAdapterNotFound)?; - - Ok(compositor) + Compositor::request(settings, Some(compatible_window)) + .await + .ok_or(Error::GraphicsAdapterNotFound) } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. @@ -232,7 +236,7 @@ impl graphics::Compositor for Compositor { fn new<W: compositor::Window>( settings: Self::Settings, compatible_window: W, - ) -> Result<Self, Error> { + ) -> impl Future<Output = Result<Self, Error>> { new(settings, compatible_window) } @@ -255,7 +259,9 @@ impl graphics::Compositor for Compositor { .create_surface(window) .expect("Create surface"); - self.configure_surface(&mut surface, width, height); + if width > 0 && height > 0 { + self.configure_surface(&mut surface, width, height); + } surface } diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c41d..3c9ffddb1c 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -25,7 +25,6 @@ wgpu = ["iced_renderer/wgpu"] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true -iced_style.workspace = true num-traits.workspace = true thiserror.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index d16e8c6787..5790f811bd 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,26 +1,22 @@ //! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::theme::palette; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, - Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -pub use crate::style::button::{Appearance, StyleSheet}; - /// A generic widget that produces a message when pressed. /// /// ```no_run -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; /// # /// #[derive(Clone)] /// enum Message { @@ -34,8 +30,7 @@ pub use crate::style::button::{Appearance, StyleSheet}; /// be disabled: /// /// ``` -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; /// # /// #[derive(Clone)] /// enum Message { @@ -53,7 +48,6 @@ pub use crate::style::button::{Appearance, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -61,18 +55,21 @@ where width: Length, height: Length, padding: Padding, - style: Theme::Style, + clip: bool, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { /// Creates a new [`Button`] with the given content. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle + 'a, + { let content = content.into(); let size = content.as_widget().size_hint(); @@ -81,8 +78,9 @@ where on_press: None, width: size.width.fluid(), height: size.height.fluid(), - padding: Padding::new(5.0), - style: Theme::Style::default(), + padding: DEFAULT_PADDING, + clip: false, + style: Box::new(Theme::default_style), } } @@ -122,17 +120,31 @@ where } /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); + self + } + + /// Sets whether the contents of the [`Button`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; self } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +struct State { + is_pressed: bool, +} + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Button<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet, Renderer: 'a + crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -140,7 +152,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn children(&self) -> Vec<Tree> { @@ -164,13 +176,19 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout(limits, self.width, self.height, self.padding, |limits| { - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - }) + layout::padded( + limits, + self.width, + self.height, + self.padding, + |limits| { + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) + }, + ) } fn operate( @@ -214,9 +232,48 @@ where return event::Status::Captured; } - update(event, layout, cursor, shell, &self.on_press, || { - tree.state.downcast_mut::<State>() - }) + 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 cursor.is_over(bounds) { + let state = tree.state.downcast_mut::<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) = self.on_press.clone() { + let state = tree.state.downcast_mut::<State>(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if cursor.is_over(bounds) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = tree.state.downcast_mut::<State>(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -227,20 +284,49 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); + let is_mouse_over = cursor.is_over(bounds); - let styling = draw( - renderer, - bounds, - cursor, - self.on_press.is_some(), - theme, - &self.style, - || tree.state.downcast_ref::<State>(), - ); + let status = if self.on_press.is_none() { + Status::Disabled + } else if is_mouse_over { + let state = tree.state.downcast_ref::<State>(); + + if state.is_pressed { + Status::Pressed + } else { + Status::Hovered + } + } else { + Status::Active + }; + + let styling = (self.style)(theme, status); + + if styling.background.is_some() + || styling.border.width > 0.0 + || styling.shadow.color.a > 0.0 + { + renderer.fill_quad( + renderer::Quad { + bounds, + border: styling.border, + shadow: styling.shadow, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + let viewport = if self.clip { + bounds.intersection(viewport).unwrap_or(*viewport) + } else { + *viewport + }; self.content.as_widget().draw( &tree.children[0], @@ -251,7 +337,7 @@ where }, content_layout, cursor, - &bounds, + &viewport, ); } @@ -263,7 +349,13 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, self.on_press.is_some()) + let is_mouse_over = cursor.is_over(layout.bounds()); + + if is_mouse_over && self.on_press.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } } fn overlay<'b>( @@ -286,7 +378,7 @@ impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { @@ -294,143 +386,182 @@ where } } -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, +/// The default [`Padding`] of a [`Button`]. +pub(crate) const DEFAULT_PADDING: Padding = Padding { + top: 5.0, + bottom: 5.0, + right: 10.0, + left: 10.0, +}; + +/// The possible status of a [`Button`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Button`] can be pressed. + Active, + /// The [`Button`] can be pressed and it is being hovered. + Hovered, + /// The [`Button`] is being pressed. + Pressed, + /// The [`Button`] cannot be pressed. + Disabled, } -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() +/// The appearance of a button. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The [`Background`] of the button. + pub background: Option<Background>, + /// The text [`Color`] of the button. + pub text_color: Color, + /// The [`Border`] of the buton. + pub border: Border, + /// The [`Shadow`] of the butoon. + pub shadow: Shadow, +} + +impl Appearance { + /// Updates the [`Appearance`] with the given [`Background`]. + pub fn with_background(self, background: impl Into<Background>) -> Self { + Self { + background: Some(background.into()), + ..self + } } } -/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] -/// accordingly. -pub fn update<'a, Message: Clone>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - 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 cursor.is_over(bounds) { - let state = state(); - - state.is_pressed = true; - - return event::Status::Captured; - } - } +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: None, + text_color: Color::BLACK, + border: Border::default(), + shadow: Shadow::default(), } - 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; +/// The style of a [`Button`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; - let bounds = layout.bounds(); +/// The default style of a [`Button`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Button`]. + fn default_style(&self, status: Status) -> Appearance; +} - if cursor.is_over(bounds) { - shell.publish(on_press); - } +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + primary(self, status) + } +} - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} - state.is_pressed = false; - } - _ => {} +impl DefaultStyle for Color { + fn default_style(&self, _status: Status) -> Appearance { + Appearance::default().with_background(*self) } +} - event::Status::Ignored +/// A primary button; denoting a main action. +pub fn primary(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + let base = styled(palette.primary.strong); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + background: Some(Background::Color(palette.primary.base.color)), + ..base + }, + Status::Disabled => disabled(base), + } } -/// Draws a [`Button`]. -pub fn draw<'a, Theme, Renderer: crate::core::Renderer>( - renderer: &mut Renderer, - bounds: Rectangle, - cursor: mouse::Cursor, - is_enabled: bool, - theme: &Theme, - style: &Theme::Style, - state: impl FnOnce() -> &'a State, -) -> Appearance -where - Theme: StyleSheet, -{ - let is_mouse_over = cursor.is_over(bounds); +/// A secondary button; denoting a complementary action. +pub fn secondary(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + let base = styled(palette.secondary.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + background: Some(Background::Color(palette.secondary.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} - let styling = if !is_enabled { - theme.disabled(style) - } else if is_mouse_over { - let state = state(); +/// A success button; denoting a good outcome. +pub fn success(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + let base = styled(palette.success.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + background: Some(Background::Color(palette.success.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} - if state.is_pressed { - theme.pressed(style) - } else { - theme.hovered(style) - } - } else { - theme.active(style) +/// A danger button; denoting a destructive action. +pub fn danger(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + let base = styled(palette.danger.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + background: Some(Background::Color(palette.danger.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} + +/// A text button; useful for links. +pub fn text(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let base = Appearance { + text_color: palette.background.base.text, + ..Appearance::default() }; - if styling.background.is_some() - || styling.border.width > 0.0 - || styling.shadow.color.a > 0.0 - { - renderer.fill_quad( - renderer::Quad { - bounds, - border: styling.border, - shadow: styling.shadow, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + text_color: palette.background.base.text.scale_alpha(0.8), + ..base + }, + Status::Disabled => disabled(base), } - - styling } -/// Computes the layout of a [`Button`]. -pub fn layout( - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&layout::Limits) -> layout::Node, -) -> layout::Node { - layout::padded(limits, width, height, padding, layout_content) +fn styled(pair: palette::Pair) -> Appearance { + Appearance { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border::rounded(2), + ..Appearance::default() + } } -/// Returns the [`mouse::Interaction`] of a [`Button`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - is_enabled: bool, -) -> mouse::Interaction { - let is_mouse_over = cursor.is_over(layout.bounds()); - - if is_mouse_over && is_enabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() +fn disabled(appearance: Appearance) -> Appearance { + Appearance { + background: appearance + .background + .map(|background| background.scale_alpha(0.5)), + text_color: appearance.text_color.scale_alpha(0.5), + ..appearance } } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 0ff4d58b51..15fb8f5844 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -5,22 +5,21 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; +use crate::core::theme::palette; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; -pub use crate::style::checkbox::{Appearance, StyleSheet}; - /// A box that can be checked. /// /// # Example /// /// ```no_run -/// # type Checkbox<'a, Message> = -/// # iced_widget::Checkbox<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>; /// # /// pub enum Message { /// CheckboxToggled(bool), @@ -39,7 +38,6 @@ pub struct Checkbox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { is_checked: bool, @@ -53,26 +51,28 @@ pub struct Checkbox< text_shaping: text::Shaping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, - style: <Theme as StyleSheet>::Style, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> where Renderer: text::Renderer, - Theme: StyleSheet + crate::text::StyleSheet, { /// The default size of a [`Checkbox`]. - const DEFAULT_SIZE: f32 = 20.0; + const DEFAULT_SIZE: f32 = 16.0; /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: f32 = 10.0; + const DEFAULT_SPACING: f32 = 8.0; /// Creates a new [`Checkbox`]. /// /// It expects: /// * the label of the [`Checkbox`] /// * a boolean describing whether the [`Checkbox`] is checked or not - pub fn new(label: impl Into<String>, is_checked: bool) -> Self { + pub fn new(label: impl Into<String>, is_checked: bool) -> Self + where + Theme: DefaultStyle + 'a, + { Checkbox { is_checked, on_toggle: None, @@ -91,7 +91,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -176,9 +176,9 @@ where /// Sets the style of the [`Checkbox`]. pub fn style( mut self, - style: impl Into<<Theme as StyleSheet>::Style>, + style: impl Fn(&Theme, Status) -> Appearance + 'a, ) -> Self { - self.style = style.into(); + self.style = Box::new(style); self } } @@ -186,7 +186,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Checkbox<'a, Message, Theme, Renderer> where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -293,17 +292,20 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); let is_disabled = self.on_toggle.is_none(); + let is_checked = self.is_checked; let mut children = layout.children(); - let custom_style = if is_disabled { - theme.disabled(&self.style, self.is_checked) + let status = if is_disabled { + Status::Disabled { is_checked } } else if is_mouse_over { - theme.hovered(&self.style, self.is_checked) + Status::Hovered { is_checked } } else { - theme.active(&self.style, self.is_checked) + Status::Active { is_checked } }; + let appearance = (self.style)(theme, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -311,10 +313,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: custom_style.border, + border: appearance.border, ..renderer::Quad::default() }, - custom_style.background, + appearance.background, ); let Icon { @@ -339,7 +341,7 @@ where shaping: *shaping, }, bounds.center(), - custom_style.icon_color, + appearance.icon_color, *viewport, ); } @@ -354,7 +356,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance { - color: custom_style.text_color, + color: appearance.text_color, }, viewport, ); @@ -366,7 +368,7 @@ impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + StyleSheet + crate::text::StyleSheet, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from( @@ -390,3 +392,183 @@ pub struct Icon<Font> { /// The shaping strategy of the icon. pub shaping: text::Shaping, } + +/// The possible status of a [`Checkbox`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Checkbox`] can be interacted with. + Active { + /// Indicates if the [`Checkbox`] is currently checked. + is_checked: bool, + }, + /// The [`Checkbox`] can be interacted with and it is being hovered. + Hovered { + /// Indicates if the [`Checkbox`] is currently checked. + is_checked: bool, + }, + /// The [`Checkbox`] cannot be interacted with. + Disabled { + /// Indicates if the [`Checkbox`] is currently checked. + is_checked: bool, + }, +} + +/// The appearance of a checkbox. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the checkbox. + pub background: Background, + /// The icon [`Color`] of the checkbox. + pub icon_color: Color, + /// The [`Border`] of hte checkbox. + pub border: Border, + /// The text [`Color`] of the checkbox. + pub text_color: Option<Color>, +} + +/// The style of a [`Checkbox`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`Checkbox`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Checkbox`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + primary(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// A primary checkbox; denoting a main toggle. +pub fn primary(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + match status { + Status::Active { is_checked } => styled( + palette.primary.strong.text, + palette.background.base, + palette.primary.strong, + is_checked, + ), + Status::Hovered { is_checked } => styled( + palette.primary.strong.text, + palette.background.weak, + palette.primary.base, + is_checked, + ), + Status::Disabled { is_checked } => styled( + palette.primary.strong.text, + palette.background.weak, + palette.background.strong, + is_checked, + ), + } +} + +/// A secondary checkbox; denoting a complementary toggle. +pub fn secondary(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + match status { + Status::Active { is_checked } => styled( + palette.background.base.text, + palette.background.base, + palette.background.strong, + is_checked, + ), + Status::Hovered { is_checked } => styled( + palette.background.base.text, + palette.background.weak, + palette.background.strong, + is_checked, + ), + Status::Disabled { is_checked } => styled( + palette.background.strong.color, + palette.background.weak, + palette.background.weak, + is_checked, + ), + } +} + +/// A success checkbox; denoting a positive toggle. +pub fn success(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + match status { + Status::Active { is_checked } => styled( + palette.success.base.text, + palette.background.base, + palette.success.base, + is_checked, + ), + Status::Hovered { is_checked } => styled( + palette.success.base.text, + palette.background.weak, + palette.success.base, + is_checked, + ), + Status::Disabled { is_checked } => styled( + palette.success.base.text, + palette.background.weak, + palette.success.weak, + is_checked, + ), + } +} + +/// A danger checkbox; denoting a negaive toggle. +pub fn danger(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + match status { + Status::Active { is_checked } => styled( + palette.danger.base.text, + palette.background.base, + palette.danger.base, + is_checked, + ), + Status::Hovered { is_checked } => styled( + palette.danger.base.text, + palette.background.weak, + palette.danger.base, + is_checked, + ), + Status::Disabled { is_checked } => styled( + palette.danger.base.text, + palette.background.weak, + palette.danger.weak, + is_checked, + ), + } +} + +fn styled( + icon_color: Color, + base: palette::Pair, + accent: palette::Pair, + is_checked: bool, +) -> Appearance { + Appearance { + background: Background::Color(if is_checked { + accent.color + } else { + base.color + }), + icon_color, + border: Border { + radius: 2.0.into(), + width: 1.0, + color: accent.color, + }, + text_color: None, + } +} diff --git a/widget/src/column.rs b/widget/src/column.rs index 1e9841eee8..d37ef69525 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -20,6 +20,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> height: Length, max_width: f32, align_items: Alignment, + clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -29,24 +30,38 @@ where { /// Creates an empty [`Column`]. pub fn new() -> Self { - Column { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self::new().extend(children) + } + + /// Creates a [`Column`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Column::width`] or [`Column::height`] accordingly. + pub fn from_vec( + children: Vec<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { spacing: 0.0, padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - children: Vec::new(), + clip: false, + children, } } - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, - ) -> Self { - children.into_iter().fold(Self::new(), Self::push) - } - /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -87,25 +102,47 @@ where self } + /// Sets whether the contents of the [`Column`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + /// Adds an element to the [`Column`]. pub fn push( mut self, child: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Self { let child = child.into(); - let size = child.as_widget().size_hint(); + let child_size = child.as_widget().size_hint(); - if size.width.is_fill() { - self.width = Length::Fill; - } - - if size.height.is_fill() { - self.height = Length::Fill; - } + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); self.children.push(child); self } + + /// Adds an element to the [`Column`], if `Some`. + pub fn push_maybe( + self, + child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Column`] with the given children. + pub fn extend( + self, + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } } impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> @@ -240,7 +277,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - if let Some(viewport) = layout.bounds().intersection(viewport) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { for ((child, state), layout) in self .children .iter() @@ -248,7 +285,17 @@ where .zip(layout.children()) { child.as_widget().draw( - state, renderer, theme, style, layout, cursor, &viewport, + state, + renderer, + theme, + style, + layout, + cursor, + if self.clip { + &clipped_viewport + } else { + viewport + }, ); } } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index e3862174ad..ee24d7429a 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -10,11 +10,11 @@ use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Vector, + Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector, }; use crate::overlay::menu; use crate::text::LineHeight; -use crate::{container, scrollable, text_input, TextInput}; +use crate::text_input::{self, TextInput}; use std::cell::RefCell; use std::fmt::Display; @@ -32,7 +32,6 @@ pub struct ComboBox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: text_input::StyleSheet + menu::StyleSheet, Renderer: text::Renderer, { state: &'a State<T>, @@ -43,7 +42,7 @@ pub struct ComboBox< on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, on_close: Option<Message>, on_input: Option<Box<dyn Fn(String) -> Message>>, - menu_style: <Theme as menu::StyleSheet>::Style, + menu_style: menu::Style<'a, Theme>, padding: Padding, size: Option<f32>, } @@ -51,7 +50,6 @@ pub struct ComboBox< impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + menu::StyleSheet, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, @@ -62,9 +60,18 @@ where placeholder: &str, selection: Option<&T>, on_selected: impl Fn(T) -> Message + 'static, - ) -> Self { - let text_input = TextInput::new(placeholder, &state.value()) - .on_input(TextInputEvent::TextChanged); + ) -> Self + where + Theme: DefaultStyle + 'a, + { + let style = Theme::default_style(); + + let text_input = TextInput::with_style( + placeholder, + &state.value(), + style.text_input, + ) + .on_input(TextInputEvent::TextChanged); let selection = selection.map(T::to_string).unwrap_or_default(); @@ -77,7 +84,7 @@ where on_option_hovered: None, on_input: None, on_close: None, - menu_style: Default::default(), + menu_style: style.menu, padding: text_input::DEFAULT_PADDING, size: None, } @@ -118,24 +125,14 @@ where } /// Sets the style of the [`ComboBox`]. - // TODO: Define its own `StyleSheet` trait - pub fn style<S>(mut self, style: S) -> Self + pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self where - S: Into<<Theme as text_input::StyleSheet>::Style> - + Into<<Theme as menu::StyleSheet>::Style> - + Clone, + Theme: 'a, { - self.menu_style = style.clone().into(); - self.text_input = self.text_input.style(style); - self - } + let style = style.into(); - /// Sets the style of the [`TextInput`] of the [`ComboBox`]. - pub fn text_input_style<S>(mut self, style: S) -> Self - where - S: Into<<Theme as text_input::StyleSheet>::Style> + Clone, - { - self.text_input = self.text_input.style(style); + self.text_input = self.text_input.style(style.text_input); + self.menu_style = style.menu; self } @@ -299,10 +296,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, - Theme: container::StyleSheet - + text_input::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -693,10 +686,10 @@ where (self.on_selected)(x) }, self.on_option_hovered.as_deref(), + &self.menu_style, ) .width(bounds.width) - .padding(self.padding) - .style(self.menu_style.clone()); + .padding(self.padding); if let Some(font) = self.font { menu = menu.font(font); @@ -719,11 +712,7 @@ impl<'a, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone + 'a, - Theme: container::StyleSheet - + text_input::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { @@ -731,8 +720,7 @@ where } } -/// Search list of options for a given query. -pub fn search<'a, T, A>( +fn search<'a, T, A>( options: impl IntoIterator<Item = T> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a, query: &'a str, @@ -759,8 +747,7 @@ where }) } -/// Build matchers from given list of options. -pub fn build_matchers<'a, T>( +fn build_matchers<'a, T>( options: impl IntoIterator<Item = T> + 'a, ) -> Vec<String> where @@ -775,3 +762,30 @@ where }) .collect() } + +/// The style of a [`ComboBox`]. +#[allow(missing_debug_implementations)] +pub struct Style<'a, Theme> { + /// The style of the [`TextInput`] of the [`ComboBox`]. + pub text_input: text_input::Style<'a, Theme>, + + /// The style of the [`Menu`] of the [`ComboBox`]. + /// + /// [`Menu`]: menu::Menu + pub menu: menu::Style<'a, Theme>, +} + +/// The default style of a [`ComboBox`]. +pub trait DefaultStyle: Sized { + /// Returns the default style of a [`ComboBox`]. + fn default_style() -> Style<'static, Self>; +} + +impl DefaultStyle for Theme { + fn default_style() -> Style<'static, Self> { + Style { + text_input: Box::new(text_input::default), + menu: menu::DefaultStyle::default_style(), + } + } +} diff --git a/widget/src/container.rs b/widget/src/container.rs index 4eb4a5d9bb..7c133588ed 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,6 +1,7 @@ //! Decorate content and apply alignment. use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; +use crate::core::gradient::{self, Gradient}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -8,13 +9,11 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, - Point, Rectangle, Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use iced_style::container::{Appearance, StyleSheet}; - /// An element decorating some content. /// /// It is normally used for alignment purposes. @@ -25,7 +24,6 @@ pub struct Container< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: crate::core::Renderer, { id: Option<Id>, @@ -36,20 +34,30 @@ pub struct Container< max_height: f32, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - style: Theme::Style, + clip: bool, content: Element<'a, Message, Theme, Renderer>, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { - /// Creates an empty [`Container`]. - pub fn new<T>(content: T) -> Self + /// Creates a [`Container`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self where - T: Into<Element<'a, Message, Theme, Renderer>>, + Theme: DefaultStyle + 'a, { + Self::with_style(content, Theme::default_style) + } + + /// Creates a [`Container`] with the given content and style. + pub fn with_style( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { let content = content.into(); let size = content.as_widget().size_hint(); @@ -62,7 +70,8 @@ where max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - style: Default::default(), + clip: false, + style: Box::new(style), content, } } @@ -128,8 +137,18 @@ where } /// Sets the style of the [`Container`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); + self + } + + /// Sets whether the contents of the [`Container`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; self } } @@ -137,7 +156,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -253,10 +271,18 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let style = theme.appearance(&self.style); + let bounds = layout.bounds(); + + let status = if cursor.is_over(bounds) { + Status::Hovered + } else { + Status::Idle + }; + + let style = (self.style)(theme, status); - if let Some(viewport) = layout.bounds().intersection(viewport) { - draw_background(renderer, &style, layout.bounds()); + if let Some(clipped_viewport) = bounds.intersection(viewport) { + draw_background(renderer, &style, bounds); self.content.as_widget().draw( tree, @@ -269,7 +295,11 @@ where }, layout.children().next().unwrap(), cursor, - &viewport, + if self.clip { + &clipped_viewport + } else { + viewport + }, ); } } @@ -294,7 +324,7 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + StyleSheet, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( @@ -469,3 +499,139 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { bounds: None, }) } + +/// The appearance of a container. +#[derive(Debug, Clone, Copy, Default)] +pub struct Appearance { + /// The text [`Color`] of the container. + pub text_color: Option<Color>, + /// The [`Background`] of the container. + pub background: Option<Background>, + /// The [`Border`] of the container. + pub border: Border, + /// The [`Shadow`] of the container. + pub shadow: Shadow, +} + +impl Appearance { + /// Updates the border of the [`Appearance`] with the given [`Color`] and `width`. + pub fn with_border( + self, + color: impl Into<Color>, + width: impl Into<Pixels>, + ) -> Self { + Self { + border: Border { + color: color.into(), + width: width.into().0, + ..Border::default() + }, + ..self + } + } + + /// Updates the background of the [`Appearance`]. + pub fn with_background(self, background: impl Into<Background>) -> Self { + Self { + background: Some(background.into()), + ..self + } + } +} + +impl From<Color> for Appearance { + fn from(color: Color) -> Self { + Self::default().with_background(color) + } +} + +impl From<Gradient> for Appearance { + fn from(gradient: Gradient) -> Self { + Self::default().with_background(gradient) + } +} + +impl From<gradient::Linear> for Appearance { + fn from(gradient: gradient::Linear) -> Self { + Self::default().with_background(gradient) + } +} + +/// The possible status of a [`Container`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Container`] is idle. + Idle, + /// The [`Container`] is being hovered. + Hovered, +} + +/// The style of a [`Container`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`Container`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Container`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + transparent(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +impl DefaultStyle for Color { + fn default_style(&self, _status: Status) -> Appearance { + Appearance::from(*self) + } +} + +impl DefaultStyle for Gradient { + fn default_style(&self, _status: Status) -> Appearance { + Appearance::from(*self) + } +} + +impl DefaultStyle for gradient::Linear { + fn default_style(&self, _status: Status) -> Appearance { + Appearance::from(*self) + } +} + +/// A transparent [`Container`]. +pub fn transparent<Theme>(_theme: &Theme, _status: Status) -> Appearance { + Appearance::default() +} + +/// A rounded [`Container`] with a background. +pub fn rounded_box(theme: &Theme, _status: Status) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background: Some(palette.background.weak.color.into()), + border: Border::rounded(2), + ..Appearance::default() + } +} + +/// A bordered [`Container`] with a background. +pub fn bordered_box(theme: &Theme, _status: Status) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 1.0, + radius: 0.0.into(), + color: palette.background.strong.color, + }, + ..Appearance::default() + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a5411c899d..4863e55066 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,7 +7,6 @@ use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; use crate::keyed; -use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -20,9 +19,10 @@ use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; -use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; +use crate::vertical_slider::{self, VerticalSlider}; +use crate::{Column, MouseArea, Row, Space, Themer}; -use std::borrow::Cow; +use std::borrow::Borrow; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -58,7 +58,7 @@ pub fn container<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Container<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, + Theme: container::DefaultStyle + 'a, Renderer: core::Renderer, { Container::new(content) @@ -104,7 +104,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Scrollable<'a, Message, Theme, Renderer> where - Theme: scrollable::StyleSheet, + Theme: scrollable::DefaultStyle + 'a, Renderer: core::Renderer, { Scrollable::new(content) @@ -117,8 +117,8 @@ pub fn button<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Button<'a, Message, Theme, Renderer> where + Theme: button::DefaultStyle + 'a, Renderer: core::Renderer, - Theme: button::StyleSheet, { Button::new(content) } @@ -134,7 +134,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>( position: tooltip::Position, ) -> crate::Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + text::StyleSheet, + Theme: container::DefaultStyle + 'a, Renderer: core::text::Renderer, { Tooltip::new(content, tooltip, position) @@ -147,7 +147,7 @@ pub fn text<'a, Theme, Renderer>( text: impl ToString, ) -> Text<'a, Theme, Renderer> where - Theme: text::StyleSheet, + Theme: text::DefaultStyle + 'a, Renderer: core::text::Renderer, { Text::new(text.to_string()) @@ -161,7 +161,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>( is_checked: bool, ) -> Checkbox<'a, Message, Theme, Renderer> where - Theme: checkbox::StyleSheet + text::StyleSheet, + Theme: checkbox::DefaultStyle + 'a, Renderer: core::text::Renderer, { Checkbox::new(label, is_checked) @@ -170,15 +170,15 @@ where /// Creates a new [`Radio`]. /// /// [`Radio`]: crate::Radio -pub fn radio<Message, Theme, Renderer, V>( +pub fn radio<'a, Message, Theme, Renderer, V>( label: impl Into<String>, value: V, selected: Option<V>, on_click: impl FnOnce(V) -> Message, -) -> Radio<Message, Theme, Renderer> +) -> Radio<'a, Message, Theme, Renderer> where Message: Clone, - Theme: radio::StyleSheet, + Theme: radio::DefaultStyle + 'a, Renderer: core::text::Renderer, V: Copy + Eq, { @@ -194,8 +194,8 @@ pub fn toggler<'a, Message, Theme, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where + Theme: toggler::DefaultStyle + 'a, Renderer: core::text::Renderer, - Theme: toggler::StyleSheet, { Toggler::new(label, is_checked, f) } @@ -209,7 +209,7 @@ pub fn text_input<'a, Message, Theme, Renderer>( ) -> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: text_input::StyleSheet, + Theme: text_input::DefaultStyle + 'a, Renderer: core::text::Renderer, { TextInput::new(placeholder, value) @@ -218,12 +218,12 @@ where /// Creates a new [`TextEditor`]. /// /// [`TextEditor`]: crate::TextEditor -pub fn text_editor<Message, Theme, Renderer>( - content: &text_editor::Content<Renderer>, -) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Theme, Renderer> +pub fn text_editor<'a, Message, Theme, Renderer>( + content: &'a text_editor::Content<Renderer>, +) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer> where Message: Clone, - Theme: text_editor::StyleSheet, + Theme: text_editor::DefaultStyle + 'a, Renderer: core::text::Renderer, { TextEditor::new(content) @@ -240,7 +240,7 @@ pub fn slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::StyleSheet, + Theme: slider::DefaultStyle + 'a, { Slider::new(range, value, on_change) } @@ -256,7 +256,7 @@ pub fn vertical_slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::StyleSheet, + Theme: vertical_slider::DefaultStyle + 'a, { VerticalSlider::new(range, value, on_change) } @@ -264,22 +264,18 @@ where /// Creates a new [`PickList`]. /// /// [`PickList`]: crate::PickList -pub fn pick_list<'a, Message, Theme, Renderer, T>( - options: impl Into<Cow<'a, [T]>>, - selected: Option<T>, +pub fn pick_list<'a, T, L, V, Message, Theme, Renderer>( + options: L, + selected: Option<V>, on_selected: impl Fn(T) -> Message + 'a, -) -> PickList<'a, T, Message, Theme, Renderer> +) -> PickList<'a, T, L, V, Message, Theme, Renderer> where - T: ToString + PartialEq + 'static, - [T]: ToOwned<Owned = Vec<T>>, + T: ToString + PartialEq + Clone + 'a, + L: Borrow<[T]> + 'a, + V: Borrow<T> + 'a, Message: Clone, + Theme: pick_list::DefaultStyle, Renderer: core::text::Renderer, - Theme: pick_list::StyleSheet - + scrollable::StyleSheet - + overlay::menu::StyleSheet - + container::StyleSheet, - <Theme as overlay::menu::StyleSheet>::Style: - From<<Theme as pick_list::StyleSheet>::Style>, { PickList::new(options, selected, on_selected) } @@ -295,32 +291,34 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>( ) -> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + overlay::menu::StyleSheet, + Theme: combo_box::DefaultStyle + 'a, Renderer: core::text::Renderer, { ComboBox::new(state, placeholder, selection, on_selected) } -/// Creates a new horizontal [`Space`] with the given [`Length`]. +/// Creates a new [`Space`] widget that fills the available +/// horizontal space. /// -/// [`Space`]: crate::Space -pub fn horizontal_space(width: impl Into<Length>) -> Space { - Space::with_width(width) +/// This can be useful to separate widgets in a [`Row`]. +pub fn horizontal_space() -> Space { + Space::with_width(Length::Fill) } -/// Creates a new vertical [`Space`] with the given [`Length`]. +/// Creates a new [`Space`] widget that fills the available +/// vertical space. /// -/// [`Space`]: crate::Space -pub fn vertical_space(height: impl Into<Length>) -> Space { - Space::with_height(height) +/// This can be useful to separate widgets in a [`Column`]. +pub fn vertical_space() -> Space { + Space::with_height(Length::Fill) } /// Creates a horizontal [`Rule`] with the given height. /// /// [`Rule`]: crate::Rule -pub fn horizontal_rule<Theme>(height: impl Into<Pixels>) -> Rule<Theme> +pub fn horizontal_rule<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme> where - Theme: rule::StyleSheet, + Theme: rule::DefaultStyle + 'a, { Rule::horizontal(height) } @@ -328,9 +326,9 @@ where /// Creates a vertical [`Rule`] with the given width. /// /// [`Rule`]: crate::Rule -pub fn vertical_rule<Theme>(width: impl Into<Pixels>) -> Rule<Theme> +pub fn vertical_rule<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme> where - Theme: rule::StyleSheet, + Theme: rule::DefaultStyle + 'a, { Rule::vertical(width) } @@ -342,12 +340,12 @@ where /// * the current value of the [`ProgressBar`]. /// /// [`ProgressBar`]: crate::ProgressBar -pub fn progress_bar<Theme>( +pub fn progress_bar<'a, Theme>( range: RangeInclusive<f32>, value: f32, -) -> ProgressBar<Theme> +) -> ProgressBar<'a, Theme> where - Theme: progress_bar::StyleSheet, + Theme: progress_bar::DefaultStyle + 'a, { ProgressBar::new(range, value) } @@ -365,9 +363,11 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle #[cfg(feature = "svg")] -pub fn svg<Theme>(handle: impl Into<core::svg::Handle>) -> crate::Svg<Theme> +pub fn svg<'a, Theme>( + handle: impl Into<core::svg::Handle>, +) -> crate::Svg<'a, Theme> where - Theme: crate::svg::StyleSheet, + Theme: crate::svg::DefaultStyle + 'a, { crate::Svg::new(handle) } @@ -386,6 +386,20 @@ where crate::Canvas::new(program) } +/// Creates a new [`QRCode`] widget from the given [`Data`]. +/// +/// [`QRCode`]: crate::QRCode +/// [`Data`]: crate::qr_code::Data +#[cfg(feature = "qr_code")] +pub fn qr_code<'a, Theme>( + data: &'a crate::qr_code::Data, +) -> crate::QRCode<'a, Theme> +where + Theme: crate::qr_code::DefaultStyle + 'a, +{ + crate::QRCode::new(data) +} + /// Creates a new [`Shader`]. /// /// [`Shader`]: crate::Shader @@ -424,12 +438,20 @@ where } /// A widget that applies any `Theme` to its contents. -pub fn themer<'a, Message, Theme, Renderer>( - theme: Theme, - content: impl Into<Element<'a, Message, Theme, Renderer>>, -) -> Themer<'a, Message, Theme, Renderer> +pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>( + new_theme: NewTheme, + content: impl Into<Element<'a, Message, NewTheme, Renderer>>, +) -> Themer< + 'a, + Message, + OldTheme, + NewTheme, + impl Fn(&OldTheme) -> NewTheme, + Renderer, +> where Renderer: core::Renderer, + NewTheme: Clone, { - Themer::new(theme, content) + Themer::new(move |_| new_theme.clone(), content) } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 9666ff9f0e..2e3fd7131c 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -40,6 +40,12 @@ impl<Handle> Viewer<Handle> { } } + /// Sets the [`image::FilterMethod`] of the [`Viewer`]. + pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self { + self.filter_method = filter_method; + self + } + /// Sets the padding of the [`Viewer`]. pub fn padding(mut self, padding: impl Into<Pixels>) -> Self { self.padding = padding.into().0; @@ -126,7 +132,7 @@ where }; // Only calculate viewport sizes if the images are constrained to a limited space. - // If they are Fill|Portion let them expand within their alotted space. + // If they are Fill|Portion let them expand within their allotted space. match expansion_size { Length::Shrink | Length::Fixed(_) => { let aspect_ratio = width as f32 / height as f32; diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 88a6e503c8..8a8d5fe7fb 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -110,20 +110,28 @@ where child: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Self { let child = child.into(); - let size = child.as_widget().size_hint(); + let child_size = child.as_widget().size_hint(); - if size.width.is_fill() { - self.width = Length::Fill; - } - - if size.height.is_fill() { - self.height = Length::Fill; - } + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); self.keys.push(key); self.children.push(child); self } + + /// Adds an element to the [`Column`], if `Some`. + pub fn push_maybe( + self, + key: Key, + child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>, + ) -> Self { + if let Some(child) = child { + self.push(key, child) + } else { + self + } + } } impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index edecbdaa6e..a512e0de91 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -62,6 +62,17 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { _operation: &mut dyn widget::Operation<Message>, ) { } + + /// Returns a [`Size`] hint for laying out the [`Component`]. + /// + /// This hint may be used by some widget containers to adjust their sizing strategy + /// during construction. + fn size_hint(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } } struct Tag<T>(T); @@ -255,10 +266,12 @@ where } fn size_hint(&self) -> Size<Length> { - Size { - width: Length::Shrink, - height: Length::Shrink, - } + self.state + .borrow() + .as_ref() + .expect("Borrow instance state") + .borrow_component() + .size_hint() } fn layout( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 44312a2158..313e1edb5b 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -50,7 +50,7 @@ where content: RefCell::new(Content { size: Size::ZERO, layout: None, - element: Element::new(horizontal_space(0)), + element: Element::new(horizontal_space().width(0)), }), } } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbebe..209dfad937 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -14,11 +14,11 @@ pub use iced_renderer as renderer; pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; -pub use iced_style as style; mod column; mod mouse_area; mod row; +mod space; mod themer; pub mod button; @@ -34,7 +34,6 @@ pub mod radio; pub mod rule; pub mod scrollable; pub mod slider; -pub mod space; pub mod text; pub mod text_editor; pub mod text_input; @@ -135,5 +134,5 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +pub use crate::core::theme::{self, Theme}; pub use renderer::Renderer; -pub use style::theme::{self, Theme}; diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 8a4d6a98cd..0364f980d4 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -10,13 +10,12 @@ use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::Tree; use crate::core::{ - Border, Clipboard, Length, Padding, Pixels, Point, Rectangle, Size, Vector, + Background, Border, Clipboard, Color, Length, Padding, Pixels, Point, + Rectangle, Size, Theme, Vector, }; use crate::core::{Element, Shell, Widget}; use crate::scrollable::{self, Scrollable}; -pub use iced_style::menu::{Appearance, StyleSheet}; - /// A list of selectable options. #[allow(missing_debug_implementations)] pub struct Menu< @@ -26,7 +25,6 @@ pub struct Menu< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { state: &'a mut State, @@ -40,24 +38,25 @@ pub struct Menu< text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: &'a Style<'a, Theme>, } impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { - /// Creates a new [`Menu`] with the given [`State`], a list of options, and - /// the message to produced when an option is selected. + /// Creates a new [`Menu`] with the given [`State`], a list of options, + /// the message to produced when an option is selected, and its [`Style`]. pub fn new( state: &'a mut State, options: &'a [T], hovered_option: &'a mut Option<usize>, on_selected: impl FnMut(T) -> Message + 'a, on_option_hovered: Option<&'a dyn Fn(T) -> Message>, + style: &'a Style<'a, Theme>, ) -> Self { Menu { state, @@ -71,7 +70,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + style, } } @@ -114,15 +113,6 @@ where self } - /// Sets the style of the [`Menu`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); - self - } - /// Turns the [`Menu`] into an overlay [`Element`] at the given target /// position. /// @@ -165,7 +155,6 @@ impl Default for State { struct Overlay<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { position: Point, @@ -173,13 +162,13 @@ where container: Container<'a, Message, Theme, Renderer>, width: f32, target_height: f32, - style: <Theme as StyleSheet>::Style, + style: &'a Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { pub fn new<T>( @@ -205,18 +194,25 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - style: style.clone(), - })); + let container = Container::with_style( + Scrollable::with_direction_and_style( + List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + style: &style.list, + }, + scrollable::Direction::default(), + &style.scrollable, + ), + container::transparent, + ); state.tree.diff(&container as &dyn Widget<_, _, _>); @@ -235,7 +231,6 @@ impl<'a, Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer> for Overlay<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -302,9 +297,10 @@ where layout: Layout<'_>, cursor: mouse::Cursor, ) { - let appearance = StyleSheet::appearance(theme, &self.style); let bounds = layout.bounds(); + let appearance = (self.style.list)(theme); + renderer.fill_quad( renderer::Quad { bounds, @@ -321,7 +317,6 @@ where struct List<'a, T, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { options: &'a [T], @@ -333,14 +328,13 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: &'a dyn Fn(&Theme) -> Appearance, } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for List<'a, T, Message, Theme, Renderer> where T: Clone + ToString, - Theme: StyleSheet, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -483,7 +477,7 @@ where _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = theme.appearance(&self.style); + let appearance = (self.style)(theme); let bounds = layout.bounds(); let text_size = @@ -517,7 +511,7 @@ where width: bounds.width - appearance.border.width * 2.0, ..bounds }, - border: Border::with_radius(appearance.border.radius), + border: Border::rounded(appearance.border.radius), ..renderer::Quad::default() }, appearance.selected_background, @@ -553,10 +547,66 @@ impl<'a, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { Element::new(list) } } + +/// The appearance of a [`Menu`]. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the menu. + pub background: Background, + /// The [`Border`] of the menu. + pub border: Border, + /// The text [`Color`] of the menu. + pub text_color: Color, + /// The text [`Color`] of a selected option in the menu. + pub selected_text_color: Color, + /// The background [`Color`] of a selected option in the menu. + pub selected_background: Background, +} + +/// The style of the different parts of a [`Menu`]. +#[allow(missing_debug_implementations)] +pub struct Style<'a, Theme> { + /// The style of the list of the [`Menu`]. + pub list: Box<dyn Fn(&Theme) -> Appearance + 'a>, + /// The style of the [`Scrollable`] of the [`Menu`]. + pub scrollable: scrollable::Style<'a, Theme>, +} + +/// The default style of a [`Menu`]. +pub trait DefaultStyle: Sized { + /// Returns the default style of a [`Menu`]. + fn default_style() -> Style<'static, Self>; +} + +impl DefaultStyle for Theme { + fn default_style() -> Style<'static, Self> { + Style { + list: Box::new(default), + scrollable: Box::new(scrollable::default), + } + } +} + +/// The default style of the list of a [`Menu`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background: palette.background.weak.color.into(), + border: Border { + width: 1.0, + radius: 0.0.into(), + color: palette.background.strong.color, + }, + text_color: palette.background.weak.text, + selected_text_color: palette.primary.strong.text, + selected_background: palette.primary.strong.color.into(), + } +} diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 4f9a265a66..beac0bd8a2 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -6,7 +6,7 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.10/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.12/examples/pane_grid mod axis; mod configuration; mod content; @@ -30,9 +30,6 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; -pub use crate::style::pane_grid::{Appearance, Line, StyleSheet}; - -use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -42,10 +39,13 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; +const DRAG_DEADBAND_DISTANCE: f32 = 10.0; +const THICKNESS_RATIO: f32 = 25.0; + /// A collection of panes distributed using either vertical or horizontal splits /// to completely fill the space available. /// @@ -70,8 +70,7 @@ use crate::core::{ /// ```no_run /// # use iced_widget::{pane_grid, text}; /// # -/// # type PaneGrid<'a, Message> = -/// # iced_widget::PaneGrid<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>; /// # /// enum PaneState { /// SomePane, @@ -102,7 +101,6 @@ pub struct PaneGrid< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, @@ -112,12 +110,11 @@ pub struct PaneGrid< on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - style: <Theme as StyleSheet>::Style, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. @@ -127,7 +124,10 @@ where pub fn new<T>( state: &'a State<T>, view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle + 'a, + { let contents = if let Some((pane, pane_state)) = state.maximized.and_then(|pane| { state.panes.get(&pane).map(|pane_state| (pane, pane_state)) @@ -158,7 +158,7 @@ where on_click: None, on_drag: None, on_resize: None, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -218,11 +218,8 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { + self.style = Box::new(style); self } @@ -237,7 +234,6 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for PaneGrid<'a, Message, Theme, Renderer> where Renderer: crate::core::Renderer, - Theme: StyleSheet + container::StyleSheet, { fn tag(&self) -> tree::Tag { tree::Tag::of::<state::Action>() @@ -282,19 +278,29 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - tree, - renderer, - limits, - self.contents.layout(), - self.width, - self.height, - self.spacing, - self.contents.iter(), - |content, tree, renderer, limits| { - content.layout(tree, renderer, limits) - }, - ) + let size = limits.resolve(self.width, self.height, Size::ZERO); + let node = self.contents.layout(); + let regions = node.pane_regions(self.spacing, size); + + let children = self + .contents + .iter() + .zip(tree.children.iter_mut()) + .filter_map(|((pane, content), tree)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let node = content.layout( + tree, + renderer, + &layout::Limits::new(size, size), + ); + + Some(node.move_to(Point::new(region.x, region.y))) + }) + .collect(); + + layout::Node::with_children(size, children) } fn operate( @@ -326,7 +332,10 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { + let mut event_status = event::Status::Ignored; + let action = tree.state.downcast_mut::<state::Action>(); + let node = self.contents.layout(); let on_drag = if self.drag_enabled() { &self.on_drag @@ -334,19 +343,164 @@ where &None }; - let event_status = update( - action, - self.contents.layout(), - &event, - layout, - cursor, - shell, - self.spacing, - self.contents.iter(), - &self.on_click, - on_drag, - &self.on_resize, - ); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if let Some(cursor_position) = cursor.position_over(bounds) { + 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 = node.split_regions( + self.spacing, + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + self.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, + self.contents.iter(), + &self.on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + self.contents.iter(), + &self.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, origin)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + if let Some(cursor_position) = cursor.position() { + if cursor_position.distance(origin) + > DRAG_DEADBAND_DISTANCE + { + let event = if let Some(edge) = + in_edge(layout, cursor_position) + { + DragEvent::Dropped { + pane, + target: Target::Edge(edge), + } + } else { + let dropped_region = self + .contents + .iter() + .zip(layout.children()) + .find_map(|(target, layout)| { + layout_region( + layout, + cursor_position, + ) + .map(|region| (target, region)) + }); + + match dropped_region { + Some(((target, _), region)) + if pane != target => + { + DragEvent::Dropped { + pane, + target: Target::Pane( + target, region, + ), + } + } + _ => DragEvent::Canceled { pane }, + } + }; + + shell.publish(on_drag(event)); + } + } + } + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + event_status = event::Status::Captured; + } + + *action = state::Action::Idle; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = &self.on_resize { + if let Some((split, _)) = action.picked_split() { + let bounds = layout.bounds(); + + let splits = node.split_regions( + self.spacing, + Size::new(bounds.width, bounds.height), + ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + if let Some(cursor_position) = cursor.position() { + let ratio = match axis { + Axis::Horizontal => { + let position = cursor_position.y + - bounds.y + - rectangle.y; + + (position / rectangle.height) + .clamp(0.1, 0.9) + } + Axis::Vertical => { + let position = cursor_position.x + - bounds.x + - rectangle.x; + + (position / rectangle.width) + .clamp(0.1, 0.9) + } + }; + + shell.publish(on_resize(ResizeEvent { + split, + ratio, + })); + + event_status = event::Status::Captured; + } + } + } + } + } + _ => {} + } let picked_pane = action.picked_pane().map(|(pane, _)| pane); @@ -380,32 +534,61 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - ) - .unwrap_or_else(|| { - self.contents - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { - content.mouse_interaction( - tree, - layout, - cursor, - viewport, - renderer, - self.drag_enabled(), + let action = tree.state.downcast_ref::<state::Action>(); + + if action.picked_pane().is_some() { + return mouse::Interaction::Grabbing; + } + + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + let node = self.contents.layout(); + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, ) + .map(|(_, axis, _)| axis) }) - .max() - .unwrap_or_default() - }) + }); + + if let Some(resize_axis) = resize_axis { + return match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }; + } + + self.contents + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor, + viewport, + renderer, + self.drag_enabled(), + ) + }) + .max() + .unwrap_or_default() } fn draw( @@ -418,28 +601,210 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - draw( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - &self.style, - self.contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), renderer, style, layout, cursor, rectangle| { - content.draw( - tree, renderer, theme, style, layout, cursor, rectangle, + let action = tree.state.downcast_ref::<state::Action>(); + let node = self.contents.layout(); + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + + let contents = self + .contents + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (pane, (content, tree))); + + let picked_pane = action.picked_pane().filter(|(_, origin)| { + cursor + .position() + .map(|position| position.distance(*origin)) + .unwrap_or_default() + > DRAG_DEADBAND_DISTANCE + }); + + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = node.split_regions(self.spacing, bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = + axis.split_line_bounds(*region, *ratio, self.spacing); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, + )?; + + Some(( + axis, + region + Vector::new(bounds.x, bounds.y), + false, + )) + } + None => None, + }); + + let pane_cursor = if picked_pane.is_some() { + mouse::Cursor::Unavailable + } else { + cursor + }; + + let mut render_picked_pane = None; + + let pane_in_edge = if picked_pane.is_some() { + cursor + .position() + .and_then(|cursor_position| in_edge(layout, cursor_position)) + } else { + None + }; + + let appearance = (self.style)(theme); + + for ((id, (content, tree)), pane_layout) in + contents.zip(layout.children()) + { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + render_picked_pane = + Some(((content, tree), origin, pane_layout)); + } + Some((dragging, _)) if id != dragging => { + content.draw( + tree, + renderer, + theme, + style, + pane_layout, + pane_cursor, + viewport, + ); + + if picked_pane.is_some() && pane_in_edge.is_none() { + if let Some(region) = + cursor.position().and_then(|cursor_position| { + layout_region(pane_layout, cursor_position) + }) + { + let bounds = + layout_region_bounds(pane_layout, region); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.hovered_region.border, + ..renderer::Quad::default() + }, + appearance.hovered_region.background, + ); + } + } + } + _ => { + content.draw( + tree, + renderer, + theme, + style, + pane_layout, + pane_cursor, + viewport, + ); + } + } + } + + if let Some(edge) = pane_in_edge { + let bounds = edge_bounds(layout, edge); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.hovered_region.border, + ..renderer::Quad::default() + }, + appearance.hovered_region.background, + ); + } + + // Render picked pane last + if let Some(((content, tree), origin, layout)) = render_picked_pane { + if let Some(cursor_position) = cursor.position() { + let bounds = layout.bounds(); + + let translation = + cursor_position - Point::new(origin.x, origin.y); + + renderer.with_translation(translation, |renderer| { + renderer.with_layer(bounds, |renderer| { + content.draw( + tree, + renderer, + theme, + style, + layout, + pane_cursor, + viewport, + ); + }); + }); + } + } + + if picked_pane.is_none() { + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + appearance.picked_split + } else { + appearance.hovered_split + }; + + 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, + }, + }, + ..renderer::Quad::default() + }, + highlight.color, ); - }, - ); + } + } } fn overlay<'b>( @@ -467,7 +832,7 @@ impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { fn from( @@ -477,255 +842,6 @@ where } } -/// Calculates the [`Layout`] of a [`PaneGrid`]. -pub fn layout<Renderer, T>( - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - node: &Node, - width: Length, - height: Length, - spacing: f32, - contents: impl Iterator<Item = (Pane, T)>, - layout_content: impl Fn( - T, - &mut Tree, - &Renderer, - &layout::Limits, - ) -> layout::Node, -) -> layout::Node { - let size = limits.resolve(width, height, Size::ZERO); - - let regions = node.pane_regions(spacing, size); - let children = contents - .zip(tree.children.iter_mut()) - .filter_map(|((pane, content), tree)| { - let region = regions.get(&pane)?; - let size = Size::new(region.width, region.height); - - let node = layout_content( - content, - tree, - renderer, - &layout::Limits::new(size, size), - ); - - Some(node.move_to(Point::new(region.x, region.y))) - }) - .collect(); - - 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, - node: &Node, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - spacing: f32, - contents: 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<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, -) -> event::Status { - const DRAG_DEADBAND_DISTANCE: f32 = 10.0; - - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if let Some(cursor_position) = cursor.position_over(bounds) { - 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 = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - 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, - contents, - on_click, - ); - } - } - None => { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - ); - } - } - } - } - 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 { - if let Some(cursor_position) = cursor.position() { - let event = if let Some(edge) = - in_edge(layout, cursor_position) - { - DragEvent::Dropped { - pane, - target: Target::Edge(edge), - } - } else { - let dropped_region = contents - .zip(layout.children()) - .find_map(|(target, layout)| { - layout_region(layout, cursor_position) - .map(|region| (target, region)) - }); - - match dropped_region { - Some(((target, _), region)) - if pane != target => - { - DragEvent::Dropped { - pane, - target: Target::Pane(target, region), - } - } - _ => DragEvent::Canceled { pane }, - } - }; - - shell.publish(on_drag(event)); - } - } - - event_status = event::Status::Captured; - } else if action.picked_split().is_some() { - event_status = event::Status::Captured; - } - - *action = state::Action::Idle; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, origin)) = action.clicked_pane() { - if let Some(on_drag) = &on_drag { - let bounds = layout.bounds(); - - if let Some(cursor_position) = cursor.position_over(bounds) - { - let mut clicked_region = contents - .zip(layout.children()) - .filter(|(_, layout)| { - layout.bounds().contains(cursor_position) - }); - - if let Some(((pane, content), layout)) = - clicked_region.next() - { - if content - .can_be_dragged_at(layout, cursor_position) - { - let pane_position = layout.position(); - - let new_origin = cursor_position - - Vector::new( - pane_position.x, - pane_position.y, - ); - - if new_origin.distance(origin) - > DRAG_DEADBAND_DISTANCE - { - *action = state::Action::Dragging { - pane, - origin, - }; - - shell.publish(on_drag(DragEvent::Picked { - pane, - })); - } - } - } - } - } - } else if let Some((_, on_resize)) = on_resize { - if let Some((split, _)) = action.picked_split() { - let bounds = layout.bounds(); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - if let Some((axis, rectangle, _)) = splits.get(&split) { - if let Some(cursor_position) = cursor.position() { - let ratio = match axis { - Axis::Horizontal => { - let position = cursor_position.y - - bounds.y - - rectangle.y; - - (position / rectangle.height) - .clamp(0.1, 0.9) - } - Axis::Vertical => { - let position = cursor_position.x - - bounds.x - - rectangle.x; - - (position / rectangle.width).clamp(0.1, 0.9) - } - }; - - shell.publish(on_resize(ResizeEvent { - split, - ratio, - })); - - event_status = event::Status::Captured; - } - } - } - } - } - _ => {} - } - - event_status -} - fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> { let bounds = layout.bounds(); @@ -755,6 +871,7 @@ fn click_pane<'a, Message, T>( shell: &mut Shell<'_, Message>, contents: impl Iterator<Item = (Pane, T)>, on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, + on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, ) where T: Draggable, { @@ -762,266 +879,24 @@ fn click_pane<'a, Message, T>( .zip(layout.children()) .filter(|(_, layout)| layout.bounds().contains(cursor_position)); - if let Some(((pane, _), layout)) = clicked_region.next() { + if let Some(((pane, content), layout)) = clicked_region.next() { if let Some(on_click) = &on_click { shell.publish(on_click(pane)); } - let pane_position = layout.position(); - let origin = - cursor_position - Vector::new(pane_position.x, pane_position.y); - *action = state::Action::Clicking { pane, origin }; - } -} - -/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. -pub fn mouse_interaction( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor: mouse::Cursor, - spacing: f32, - resize_leeway: Option<f32>, -) -> Option<mouse::Interaction> { - if action.clicked_pane().is_some() || action.picked_pane().is_some() { - return Some(mouse::Interaction::Grabbing); - } - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split(splits.iter(), 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 -} - -/// Draws a [`PaneGrid`]. -pub fn draw<Theme, Renderer, T>( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &mut Renderer, - theme: &Theme, - default_style: &renderer::Style, - viewport: &Rectangle, - spacing: f32, - resize_leeway: Option<f32>, - style: &Theme::Style, - contents: impl Iterator<Item = (Pane, T)>, - draw_pane: impl Fn( - T, - &mut Renderer, - &renderer::Style, - Layout<'_>, - mouse::Cursor, - &Rectangle, - ), -) where - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let picked_pane = action.picked_pane(); - - let picked_split = action - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds(*region, *ratio, spacing); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match resize_leeway { - Some(leeway) => { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - )?; - - Some((axis, region + Vector::new(bounds.x, bounds.y), false)) - } - None => None, - }); - - let pane_cursor = if picked_pane.is_some() { - mouse::Cursor::Unavailable - } else { - cursor - }; - - let mut render_picked_pane = None; - - let pane_in_edge = if picked_pane.is_some() { - cursor - .position() - .and_then(|cursor_position| in_edge(layout, cursor_position)) - } else { - None - }; - - for ((id, pane), pane_layout) in contents.zip(layout.children()) { - match picked_pane { - Some((dragging, origin)) if id == dragging => { - render_picked_pane = Some((pane, origin, pane_layout)); - } - Some((dragging, _)) if id != dragging => { - draw_pane( + if let Some(on_drag) = &on_drag { + if content.can_be_dragged_at(layout, cursor_position) { + *action = state::Action::Dragging { pane, - renderer, - default_style, - pane_layout, - pane_cursor, - viewport, - ); + origin: cursor_position, + }; - if picked_pane.is_some() && pane_in_edge.is_none() { - if let Some(region) = - cursor.position().and_then(|cursor_position| { - layout_region(pane_layout, cursor_position) - }) - { - let bounds = layout_region_bounds(pane_layout, region); - let hovered_region_style = theme.hovered_region(style); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: hovered_region_style.border, - ..renderer::Quad::default() - }, - theme.hovered_region(style).background, - ); - } - } - } - _ => { - draw_pane( - pane, - renderer, - default_style, - pane_layout, - pane_cursor, - viewport, - ); - } - } - } - - if let Some(edge) = pane_in_edge { - let hovered_region_style = theme.hovered_region(style); - let bounds = edge_bounds(layout, edge); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: hovered_region_style.border, - ..renderer::Quad::default() - }, - theme.hovered_region(style).background, - ); - } - - // Render picked pane last - if let Some((pane, origin, layout)) = render_picked_pane { - if let Some(cursor_position) = cursor.position() { - let bounds = layout.bounds(); - - let translation = cursor_position - - Point::new(bounds.x + origin.x, bounds.y + origin.y); - - renderer.with_translation(translation, |renderer| { - renderer.with_layer(bounds, |renderer| { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor, - viewport, - ); - }); - }); - } - } - - if picked_pane.is_none() { - 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, - }, - }, - ..renderer::Quad::default() - }, - highlight.color, - ); + shell.publish(on_drag(DragEvent::Picked { pane })); } } } } -const THICKNESS_RATIO: f32 = 25.0; - fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> { let bounds = layout.bounds(); @@ -1238,3 +1113,82 @@ impl<'a, T> Contents<'a, T> { matches!(self, Self::Maximized(..)) } } + +/// The appearance of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The appearance of a hovered region highlight. + pub hovered_region: Highlight, + /// The appearance of a picked split. + pub picked_split: Line, + /// The appearance of a hovered split. + pub hovered_split: Line, +} + +/// The appearance of a highlight of the [`PaneGrid`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Highlight { + /// The [`Background`] of the pane region. + pub background: Background, + /// The [`Border`] of the pane region. + pub border: Border, +} + +/// A line. +/// +/// It is normally used to define the highlight of something, like a split. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Line { + /// The [`Color`] of the [`Line`]. + pub color: Color, + /// The width of the [`Line`]. + pub width: f32, +} + +/// The style of a [`PaneGrid`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; + +/// The default style of a [`PaneGrid`]. +pub trait DefaultStyle { + /// Returns the default style of a [`PaneGrid`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self) -> Appearance { + *self + } +} + +/// The default style of a [`PaneGrid`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + hovered_region: Highlight { + background: Background::Color(Color { + a: 0.5, + ..palette.primary.base.color + }), + border: Border { + width: 2.0, + color: palette.primary.strong.color, + radius: 0.0.into(), + }, + }, + hovered_split: Line { + color: palette.primary.base.color, + width: 2.0, + }, + picked_split: Line { + color: palette.primary.strong.color, + width: 2.0, + }, + } +} diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index dfe0fdcfbd..98f4f99af3 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -20,25 +20,26 @@ pub struct Content< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>, body: Element<'a, Message, Theme, Renderer>, - style: Theme::Style, + style: container::Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { /// Creates a new [`Content`] with the provided body. - pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self { + pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self + where + Theme: container::DefaultStyle + 'a, + { Self { title_bar: None, body: body.into(), - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -52,15 +53,17 @@ where } /// Sets the style of the [`Content`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { pub(super) fn state(&self) -> Tree { @@ -104,7 +107,15 @@ where let bounds = layout.bounds(); { - let style = theme.appearance(&self.style); + let style = { + let status = if cursor.is_over(bounds) { + container::Status::Hovered + } else { + container::Status::Idle + }; + + (self.style)(theme, status) + }; container::draw_background(renderer, &style, bounds); } @@ -370,7 +381,6 @@ where impl<'a, Message, Theme, Renderer> Draggable for &Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { fn can_be_dragged_at( @@ -393,7 +403,7 @@ impl<'a, T, Message, Theme, Renderer> From<T> for Content<'a, Message, Theme, Renderer> where T: Into<Element<'a, Message, Theme, Renderer>>, - Theme: container::StyleSheet, + Theme: container::DefaultStyle + 'a, Renderer: crate::core::Renderer, { fn from(element: T) -> Self { diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 5d1fe254c6..481cd7709f 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -403,15 +403,6 @@ pub enum Action { /// /// [`PaneGrid`]: super::PaneGrid Idle, - /// A [`Pane`] in the [`PaneGrid`] is being clicked. - /// - /// [`PaneGrid`]: super::PaneGrid - Clicking { - /// The [`Pane`] being clicked. - pane: Pane, - /// The starting [`Point`] of the click interaction. - origin: Point, - }, /// A [`Pane`] in the [`PaneGrid`] is being dragged. /// /// [`PaneGrid`]: super::PaneGrid @@ -441,14 +432,6 @@ impl Action { } } - /// Returns the current [`Pane`] that is being clicked, if any. - pub fn clicked_pane(&self) -> Option<(Pane, Point)> { - match *self { - Action::Clicking { pane, origin, .. } => Some((pane, origin)), - _ => None, - } - } - /// Returns the current [`Split`] that is being dragged, if any. pub fn picked_split(&self) -> Option<(Split, Axis)> { match *self { diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 5b57509b35..8dfea6e319 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -19,32 +19,32 @@ pub struct TitleBar< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, controls: Option<Element<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, - style: Theme::Style, + style: container::Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { /// Creates a new [`TitleBar`] with the given content. - pub fn new<E>(content: E) -> Self + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self where - E: Into<Element<'a, Message, Theme, Renderer>>, + Theme: container::DefaultStyle + 'a, { Self { content: content.into(), controls: None, padding: Padding::ZERO, always_show_controls: false, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -64,8 +64,11 @@ where } /// Sets the style of the [`TitleBar`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } @@ -85,7 +88,6 @@ where impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { pub(super) fn state(&self) -> Tree { @@ -128,7 +130,17 @@ where show_controls: bool, ) { let bounds = layout.bounds(); - let style = theme.appearance(&self.style); + + let style = { + let status = if cursor.is_over(bounds) { + container::Status::Hovered + } else { + container::Status::Idle + }; + + (self.style)(theme, status) + }; + let inherited_style = renderer::Style { text_color: style.text_color.unwrap_or(inherited_style.text_color), }; diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 840c94fab2..52d5439705 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -1,5 +1,4 @@ //! Display a dropdown list of selectable values. -use crate::container; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -11,35 +10,36 @@ use crate::core::text::{self, Paragraph as _, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::overlay::menu::{self, Menu}; -use crate::scrollable; -use std::borrow::Cow; - -pub use crate::style::pick_list::{Appearance, StyleSheet}; +use std::borrow::Borrow; +use std::f32; /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] pub struct PickList< 'a, T, + L, + V, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where - [T]: ToOwned<Owned = Vec<T>>, - Theme: StyleSheet, + T: ToString + PartialEq + Clone, + L: Borrow<[T]> + 'a, + V: Borrow<T> + 'a, Renderer: text::Renderer, { on_select: Box<dyn Fn(T) -> Message + 'a>, on_open: Option<Message>, on_close: Option<Message>, - options: Cow<'a, [T]>, + options: L, placeholder: Option<String>, - selected: Option<T>, + selected: Option<V>, width: Length, padding: Padding, text_size: Option<Pixels>, @@ -47,47 +47,43 @@ pub struct PickList< text_shaping: text::Shaping, font: Option<Renderer::Font>, handle: Handle<Renderer::Font>, - style: Theme::Style, + style: Style<'a, Theme>, } -impl<'a, T: 'a, Message, Theme, Renderer> - PickList<'a, T, Message, Theme, Renderer> +impl<'a, T, L, V, Message, Theme, Renderer> + PickList<'a, T, L, V, Message, Theme, Renderer> where - T: ToString + PartialEq, - [T]: ToOwned<Owned = Vec<T>>, + T: ToString + PartialEq + Clone, + L: Borrow<[T]> + 'a, + V: Borrow<T> + 'a, Message: Clone, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, Renderer: text::Renderer, { - /// The default padding of a [`PickList`]. - pub const DEFAULT_PADDING: Padding = Padding::new(5.0); - /// 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( - options: impl Into<Cow<'a, [T]>>, - selected: Option<T>, + options: L, + selected: Option<V>, on_select: impl Fn(T) -> Message + 'a, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle, + { Self { on_select: Box::new(on_select), on_open: None, on_close: None, - options: options.into(), + options, placeholder: None, selected, width: Length::Shrink, - padding: Self::DEFAULT_PADDING, + padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, handle: Handle::default(), - style: Default::default(), + style: Theme::default_style(), } } @@ -155,26 +151,19 @@ where } /// Sets the style of the [`PickList`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { + pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self { self.style = style.into(); self } } -impl<'a, T: 'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for PickList<'a, T, Message, Theme, Renderer> +impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for PickList<'a, T, L, V, Message, Theme, Renderer> where - T: Clone + ToString + PartialEq + 'static, - [T]: ToOwned<Owned = Vec<T>>, + T: Clone + ToString + PartialEq + 'a, + L: Borrow<[T]>, + V: Borrow<T>, Message: Clone + 'a, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, Renderer: text::Renderer + 'a, { fn tag(&self) -> tree::Tag { @@ -198,19 +187,77 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - renderer, - limits, - self.width, - self.padding, - self.text_size, - self.text_line_height, - self.text_shaping, - self.font, - self.placeholder.as_deref(), - &self.options, - ) + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); + let options = self.options.borrow(); + + state.options.resize_with(options.len(), Default::default); + + let option_text = Text { + content: "", + bounds: Size::new( + f32::INFINITY, + self.text_line_height.to_absolute(text_size).into(), + ), + size: text_size, + line_height: self.text_line_height, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: self.text_shaping, + }; + + for (option, paragraph) in options.iter().zip(state.options.iter_mut()) + { + let label = option.to_string(); + + paragraph.update(Text { + content: &label, + ..option_text + }); + } + + if let Some(placeholder) = &self.placeholder { + state.placeholder.update(Text { + content: placeholder, + ..option_text + }); + } + + let max_width = match self.width { + Length::Shrink => { + let labels_width = + state.options.iter().fold(0.0, |width, paragraph| { + f32::max(width, paragraph.min_width()) + }); + + labels_width.max( + self.placeholder + .as_ref() + .map(|_| state.placeholder.min_width()) + .unwrap_or(0.0), + ) + } + _ => 0.0, + }; + + let size = { + let intrinsic = Size::new( + max_width + text_size.0 + self.padding.left, + f32::from(self.text_line_height.to_absolute(text_size)), + ); + + limits + .width(self.width) + .shrink(self.padding) + .resolve(self.width, Length::Shrink, intrinsic) + .expand(self.padding) + }; + + layout::Node::new(size) } fn on_event( @@ -224,18 +271,98 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - self.on_select.as_ref(), - self.on_open.as_ref(), - self.on_close.as_ref(), - self.selected.as_ref(), - &self.options, - || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - ) + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = + tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + if state.is_open { + // Event wasn't processed by overlay, so cursor was clicked either outside its + // bounds or on the drop-down, either way we close the overlay. + state.is_open = false; + + if let Some(on_close) = &self.on_close { + shell.publish(on_close.clone()); + } + + event::Status::Captured + } else if cursor.is_over(layout.bounds()) { + let selected = self.selected.as_ref().map(Borrow::borrow); + + state.is_open = true; + state.hovered_option = self + .options + .borrow() + .iter() + .position(|option| Some(option) == selected); + + if let Some(on_open) = &self.on_open { + shell.publish(on_open.clone()); + } + + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = + tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + if state.keyboard_modifiers.command() + && cursor.is_over(layout.bounds()) + && !state.is_open + { + fn find_next<'a, T: PartialEq>( + selected: &'a T, + mut options: impl Iterator<Item = &'a T>, + ) -> Option<&'a T> { + let _ = options.find(|&option| option == selected); + + options.next() + } + + let options = self.options.borrow(); + let selected = self.selected.as_ref().map(Borrow::borrow); + + let next_option = if y < 0.0 { + if let Some(selected) = selected { + find_next(selected, options.iter()) + } else { + options.first() + } + } else if y > 0.0 { + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) + } else { + options.last() + } + } else { + None + }; + + if let Some(next_option) = next_option { + shell.publish((self.on_select)(next_option.clone())); + } + + event::Status::Captured + } else { + event::Status::Ignored + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = + tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } } fn mouse_interaction( @@ -246,7 +373,14 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor) + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } } fn draw( @@ -260,23 +394,124 @@ where viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); - draw( - renderer, - theme, - layout, - cursor, - self.padding, - self.text_size, - self.text_line_height, - self.text_shaping, - font, - self.placeholder.as_deref(), - self.selected.as_ref(), - &self.handle, - &self.style, - || tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - viewport, + let selected = self.selected.as_ref().map(Borrow::borrow); + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + let is_selected = selected.is_some(); + + let status = if state.is_open { + Status::Opened + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }; + + let appearance = (self.style.field)(theme, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.border, + ..renderer::Quad::default() + }, + appearance.background, ); + + let handle = match &self.handle { + Handle::Arrow { size } => Some(( + Renderer::ICON_FONT, + Renderer::ARROW_DOWN_ICON, + *size, + text::LineHeight::default(), + text::Shaping::Basic, + )), + Handle::Static(Icon { + font, + code_point, + size, + line_height, + shaping, + }) => Some((*font, *code_point, *size, *line_height, *shaping)), + Handle::Dynamic { open, closed } => { + if state.is_open { + Some(( + open.font, + open.code_point, + open.size, + open.line_height, + open.shaping, + )) + } else { + Some(( + closed.font, + closed.code_point, + closed.size, + closed.line_height, + closed.shaping, + )) + } + } + Handle::None => None, + }; + + if let Some((font, code_point, size, line_height, shaping)) = handle { + let size = size.unwrap_or_else(|| renderer.default_size()); + + renderer.fill_text( + Text { + content: &code_point.to_string(), + size, + line_height, + font, + bounds: Size::new( + bounds.width, + f32::from(line_height.to_absolute(size)), + ), + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + shaping, + }, + Point::new( + bounds.x + bounds.width - self.padding.right, + bounds.center_y(), + ), + appearance.handle_color, + *viewport, + ); + } + + let label = selected.map(ToString::to_string); + + if let Some(label) = label.as_deref().or(self.placeholder.as_deref()) { + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); + + renderer.fill_text( + Text { + content: label, + size: text_size, + line_height: self.text_line_height, + font, + bounds: Size::new( + bounds.width - self.padding.horizontal(), + f32::from(self.text_line_height.to_absolute(text_size)), + ), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: self.text_shaping, + }, + Point::new(bounds.x + self.padding.left, bounds.center_y()), + if is_selected { + appearance.text_color + } else { + appearance.placeholder_color + }, + *viewport, + ); + } } fn overlay<'b>( @@ -287,45 +522,61 @@ where translation: Vector, ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); + + if state.is_open { + let bounds = layout.bounds(); - overlay( - layout, - translation, - state, - self.padding, - self.text_size, - self.text_shaping, - self.font.unwrap_or_else(|| renderer.default_font()), - &self.options, - &self.on_select, - self.style.clone(), - ) + let on_select = &self.on_select; + + let mut menu = Menu::new( + &mut state.menu, + self.options.borrow(), + &mut state.hovered_option, + |option| { + state.is_open = false; + + (on_select)(option) + }, + None, + &self.style.menu, + ) + .width(bounds.width) + .padding(self.padding) + .font(font) + .text_shaping(self.text_shaping); + + if let Some(text_size) = self.text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position() + translation, bounds.height)) + } else { + None + } } } -impl<'a, T: 'a, Message, Theme, Renderer> - From<PickList<'a, T, Message, Theme, Renderer>> +impl<'a, T, L, V, Message, Theme, Renderer> + From<PickList<'a, T, L, V, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where - T: Clone + ToString + PartialEq + 'static, - [T]: ToOwned<Owned = Vec<T>>, + T: Clone + ToString + PartialEq + 'a, + L: Borrow<[T]> + 'a, + V: Borrow<T> + 'a, Message: Clone + 'a, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet - + 'a, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, + Theme: 'a, Renderer: text::Renderer + 'a, { - fn from(pick_list: PickList<'a, T, Message, Theme, Renderer>) -> Self { + fn from( + pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>, + ) -> Self { Self::new(pick_list) } } -/// The state of a [`PickList`]. #[derive(Debug)] -pub struct State<P: text::Paragraph> { +struct State<P: text::Paragraph> { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, @@ -398,394 +649,81 @@ pub struct Icon<Font> { pub shaping: text::Shaping, } -/// Computes the layout of a [`PickList`]. -pub fn layout<Renderer, T>( - state: &mut State<Renderer::Paragraph>, - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - text_size: Option<Pixels>, - text_line_height: text::LineHeight, - text_shaping: text::Shaping, - font: Option<Renderer::Font>, - placeholder: Option<&str>, - options: &[T], -) -> layout::Node -where - Renderer: text::Renderer, - T: ToString, -{ - use std::f32; - - let font = font.unwrap_or_else(|| renderer.default_font()); - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - state.options.resize_with(options.len(), Default::default); - - let option_text = Text { - content: "", - bounds: Size::new( - f32::INFINITY, - text_line_height.to_absolute(text_size).into(), - ), - size: text_size, - line_height: text_line_height, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text_shaping, - }; - - for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { - let label = option.to_string(); - - paragraph.update(Text { - content: &label, - ..option_text - }); - } - - if let Some(placeholder) = placeholder { - state.placeholder.update(Text { - content: placeholder, - ..option_text - }); - } - - let max_width = match width { - Length::Shrink => { - let labels_width = - state.options.iter().fold(0.0, |width, paragraph| { - f32::max(width, paragraph.min_width()) - }); - - labels_width.max( - placeholder - .map(|_| state.placeholder.min_width()) - .unwrap_or(0.0), - ) - } - _ => 0.0, - }; - - let size = { - let intrinsic = Size::new( - max_width + text_size.0 + padding.left, - f32::from(text_line_height.to_absolute(text_size)), - ); - - limits - .width(width) - .shrink(padding) - .resolve(width, Length::Shrink, intrinsic) - .expand(padding) - }; - - layout::Node::new(size) +/// The possible status of a [`PickList`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`PickList`] can be interacted with. + Active, + /// The [`PickList`] is being hovered. + Hovered, + /// The [`PickList`] is open. + Opened, } -/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] -/// accordingly. -pub fn update<'a, T, P, Message>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - on_select: &dyn Fn(T) -> Message, - on_open: Option<&Message>, - on_close: Option<&Message>, - selected: Option<&T>, - options: &[T], - state: impl FnOnce() -> &'a mut State<P>, -) -> event::Status -where - T: PartialEq + Clone + 'a, - P: text::Paragraph + 'a, - Message: Clone, -{ - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - if state.is_open { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - - if let Some(on_close) = on_close { - shell.publish(on_close.clone()); - } - - event::Status::Captured - } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = - options.iter().position(|option| Some(option) == selected); - - if let Some(on_open) = on_open { - shell.publish(on_open.clone()); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) => { - let state = state(); - - if state.keyboard_modifiers.command() - && cursor.is_over(layout.bounds()) - && !state.is_open - { - fn find_next<'a, T: PartialEq>( - selected: &'a T, - mut options: impl Iterator<Item = &'a T>, - ) -> Option<&'a T> { - let _ = options.find(|&option| option == selected); - - options.next() - } - - let next_option = if y < 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter()) - } else { - options.first() - } - } else if y > 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter().rev()) - } else { - options.last() - } - } else { - None - }; - - if let Some(next_option) = next_option { - shell.publish((on_select)(next_option.clone())); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); +/// The appearance of a pick list. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The text [`Color`] of the pick list. + pub text_color: Color, + /// The placeholder [`Color`] of the pick list. + pub placeholder_color: Color, + /// The handle [`Color`] of the pick list. + pub handle_color: Color, + /// The [`Background`] of the pick list. + pub background: Background, + /// The [`Border`] of the pick list. + pub border: Border, +} - state.keyboard_modifiers = modifiers; +/// The styles of the different parts of a [`PickList`]. +#[allow(missing_debug_implementations)] +pub struct Style<'a, Theme> { + /// The style of the [`PickList`] itself. + pub field: Box<dyn Fn(&Theme, Status) -> Appearance + 'a>, - event::Status::Ignored - } - _ => event::Status::Ignored, - } + /// The style of the [`Menu`] of the pick list. + pub menu: menu::Style<'a, Theme>, } -/// Returns the current [`mouse::Interaction`] of a [`PickList`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } +/// The default style of a [`PickList`]. +pub trait DefaultStyle: Sized { + /// Returns the default style of a [`PickList`]. + fn default_style() -> Style<'static, Self>; } -/// Returns the current overlay of a [`PickList`]. -pub fn overlay<'a, T, Message, Theme, Renderer>( - layout: Layout<'_>, - translation: Vector, - state: &'a mut State<Renderer::Paragraph>, - padding: Padding, - text_size: Option<Pixels>, - text_shaping: text::Shaping, - font: Renderer::Font, - options: &'a [T], - on_selected: &'a dyn Fn(T) -> Message, - style: <Theme as StyleSheet>::Style, -) -> Option<overlay::Element<'a, Message, Theme, Renderer>> -where - T: Clone + ToString, - Message: 'a, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet - + 'a, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, - Renderer: text::Renderer + 'a, -{ - if state.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut state.menu, - options, - &mut state.hovered_option, - |option| { - state.is_open = false; - - (on_selected)(option) - }, - None, - ) - .width(bounds.width) - .padding(padding) - .font(font) - .text_shaping(text_shaping) - .style(style); - - if let Some(text_size) = text_size { - menu = menu.text_size(text_size); +impl DefaultStyle for Theme { + fn default_style() -> Style<'static, Self> { + Style { + field: Box::new(default), + menu: menu::DefaultStyle::default_style(), } - - Some(menu.overlay(layout.position() + translation, bounds.height)) - } else { - None } } -/// Draws a [`PickList`]. -pub fn draw<'a, T, Theme, Renderer>( - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - padding: Padding, - text_size: Option<Pixels>, - text_line_height: text::LineHeight, - text_shaping: text::Shaping, - font: Renderer::Font, - placeholder: Option<&str>, - selected: Option<&T>, - handle: &Handle<Renderer::Font>, - style: &Theme::Style, - state: impl FnOnce() -> &'a State<Renderer::Paragraph>, - viewport: &Rectangle, -) where - Renderer: text::Renderer, - Theme: StyleSheet, - T: ToString + 'a, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - 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: style.border, - ..renderer::Quad::default() +/// The default style of the field of a [`PickList`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + text_color: palette.background.weak.text, + background: palette.background.weak.color.into(), + placeholder_color: palette.background.strong.color, + handle_color: palette.background.weak.text, + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.background.strong.color, }, - style.background, - ); - - let handle = match handle { - Handle::Arrow { size } => Some(( - Renderer::ICON_FONT, - Renderer::ARROW_DOWN_ICON, - *size, - text::LineHeight::default(), - text::Shaping::Basic, - )), - Handle::Static(Icon { - font, - code_point, - size, - line_height, - shaping, - }) => Some((*font, *code_point, *size, *line_height, *shaping)), - Handle::Dynamic { open, closed } => { - if state().is_open { - Some(( - open.font, - open.code_point, - open.size, - open.line_height, - open.shaping, - )) - } else { - Some(( - closed.font, - closed.code_point, - closed.size, - closed.line_height, - closed.shaping, - )) - } - } - Handle::None => None, }; - if let Some((font, code_point, size, line_height, shaping)) = handle { - let size = size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text( - Text { - content: &code_point.to_string(), - size, - line_height, - font, - bounds: Size::new( - bounds.width, - f32::from(line_height.to_absolute(size)), - ), - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - shaping, - }, - Point::new( - bounds.x + bounds.width - padding.horizontal(), - bounds.center_y(), - ), - style.handle_color, - *viewport, - ); - } - - let label = selected.map(ToString::to_string); - - if let Some(label) = label.as_deref().or(placeholder) { - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text( - Text { - content: label, - size: text_size, - line_height: text_line_height, - font, - bounds: Size::new( - bounds.width - padding.horizontal(), - f32::from(text_line_height.to_absolute(text_size)), - ), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text_shaping, - }, - Point::new(bounds.x + padding.left, bounds.center_y()), - if is_selected { - style.text_color - } else { - style.placeholder_color + match status { + Status::Active => active, + Status::Hovered | Status::Opened => Appearance { + border: Border { + color: palette.primary.strong.color, + ..active.border }, - *viewport, - ); + ..active + }, } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 694fdd28ad..38d8da85b7 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -3,17 +3,17 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; -use crate::core::{Border, Element, Layout, Length, Rectangle, Size, Widget}; +use crate::core::{ + Background, Border, Element, Layout, Length, Rectangle, Size, Theme, Widget, +}; use std::ops::RangeInclusive; -pub use iced_style::progress_bar::{Appearance, StyleSheet}; - /// A bar that displays progress. /// /// # Example /// ```no_run -/// # type ProgressBar = iced_widget::ProgressBar<iced_widget::style::Theme>; +/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>; /// # /// let value = 50.0; /// @@ -22,21 +22,15 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet}; /// /// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) #[allow(missing_debug_implementations)] -pub struct ProgressBar<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct ProgressBar<'a, Theme = crate::Theme> { range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Theme::Style, + style: Style<'a, Theme>, } -impl<Theme> ProgressBar<Theme> -where - Theme: StyleSheet, -{ +impl<'a, Theme> ProgressBar<'a, Theme> { /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: f32 = 30.0; @@ -45,13 +39,16 @@ where /// It expects: /// * an inclusive range of possible values /// * the current value of the [`ProgressBar`] - pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { + pub fn new(range: RangeInclusive<f32>, value: f32) -> Self + where + Theme: DefaultStyle + 'a, + { ProgressBar { value: value.clamp(*range.start(), *range.end()), range, width: Length::Fill, height: None, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -68,17 +65,16 @@ where } /// Sets the style of the [`ProgressBar`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { + self.style = Box::new(style); self } } -impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for ProgressBar<Theme> +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for ProgressBar<'a, Theme> where Renderer: crate::core::Renderer, - Theme: StyleSheet, { fn size(&self) -> Size<Length> { Size { @@ -120,15 +116,15 @@ where / (range_end - range_start) }; - let style = theme.appearance(&self.style); + let appearance = (self.style)(theme); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border: Border::with_radius(style.border_radius), + border: appearance.border, ..renderer::Quad::default() }, - style.background, + appearance.background, ); if active_progress_width > 0.0 { @@ -138,25 +134,102 @@ where width: active_progress_width, ..bounds }, - border: Border::with_radius(style.border_radius), + border: Border::rounded(appearance.border.radius), ..renderer::Quad::default() }, - style.bar, + appearance.bar, ); } } } -impl<'a, Message, Theme, Renderer> From<ProgressBar<Theme>> +impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( - progress_bar: ProgressBar<Theme>, + progress_bar: ProgressBar<'a, Theme>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(progress_bar) } } + +/// The appearance of a progress bar. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the progress bar. + pub background: Background, + /// The [`Background`] of the bar of the progress bar. + pub bar: Background, + /// The [`Border`] of the progress bar. + pub border: Border, +} + +/// The style of a [`ProgressBar`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; + +/// The default style of a [`ProgressBar`]. +pub trait DefaultStyle { + /// Returns the default style of a [`ProgressBar`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + primary(self) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self) -> Appearance { + *self + } +} + +/// The primary style of a [`ProgressBar`]. +pub fn primary(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + styled( + palette.background.strong.color, + palette.primary.strong.color, + ) +} + +/// The secondary style of a [`ProgressBar`]. +pub fn secondary(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + styled( + palette.background.strong.color, + palette.secondary.base.color, + ) +} + +/// The success style of a [`ProgressBar`]. +pub fn success(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + styled(palette.background.strong.color, palette.success.base.color) +} + +/// The danger style of a [`ProgressBar`]. +pub fn danger(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + styled(palette.background.strong.color, palette.danger.base.color) +} + +fn styled( + background: impl Into<Background>, + bar: impl Into<Background>, +) -> Appearance { + Appearance { + background: background.into(), + bar: bar.into(), + border: Border::rounded(2), + } +} diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index eeb1526f9c..90c0c97044 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -5,7 +5,8 @@ use crate::core::mouse; use crate::core::renderer::{self, Renderer as _}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, + Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector, + Widget, }; use crate::graphics::geometry::Renderer as _; use crate::Renderer; @@ -13,33 +14,28 @@ use crate::Renderer; use std::cell::RefCell; use thiserror::Error; -pub use crate::style::qr_code::{Appearance, StyleSheet}; - const DEFAULT_CELL_SIZE: u16 = 4; const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. -#[derive(Debug)] -pub struct QRCode<'a, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +#[allow(missing_debug_implementations)] +pub struct QRCode<'a, Theme = crate::Theme> { data: &'a Data, cell_size: u16, - style: Theme::Style, + style: Style<'a, Theme>, } -impl<'a, Theme> QRCode<'a, Theme> -where - Theme: StyleSheet, -{ +impl<'a, Theme> QRCode<'a, Theme> { /// Creates a new [`QRCode`] with the provided [`Data`]. - pub fn new(data: &'a Data) -> Self { + pub fn new(data: &'a Data) -> Self + where + Theme: DefaultStyle + 'a, + { Self { data, cell_size: DEFAULT_CELL_SIZE, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -50,15 +46,14 @@ where } /// Sets the style of the [`QRCode`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { + self.style = Box::new(style); self } } -impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> -where - Theme: StyleSheet, +impl<'a, Message, Theme> Widget<Message, Theme, Renderer> + for QRCode<'a, Theme> { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -102,7 +97,7 @@ where let bounds = layout.bounds(); let side_length = self.data.width + 2 * QUIET_ZONE; - let appearance = theme.appearance(&self.style); + let appearance = (self.style)(theme); let mut last_appearance = state.last_appearance.borrow_mut(); if Some(appearance) != *last_appearance { @@ -156,7 +151,7 @@ where impl<'a, Message, Theme> From<QRCode<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: 'a, { fn from(qr_code: QRCode<'a, Theme>) -> Self { Self::new(qr_code) @@ -330,3 +325,43 @@ impl From<qrcode::types::QrError> for Error { struct State { last_appearance: RefCell<Option<Appearance>>, } + +/// The appearance of a QR code. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The color of the QR code data cells + pub cell: Color, + /// The color of the QR code background + pub background: Color, +} + +/// The style of a [`QRCode`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; + +/// The default style of a [`QRCode`]. +pub trait DefaultStyle { + /// Returns the default style of a [`QRCode`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self) -> Appearance { + *self + } +} + +/// The default style of a [`QRCode`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.palette(); + + Appearance { + cell: palette.text, + background: palette.background, + } +} diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 68e9bc7eff..a7b7dd036a 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -9,18 +9,16 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, - Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; -pub use iced_style::radio::{Appearance, StyleSheet}; - /// A circular button representing a choice. /// /// # Example /// ```no_run -/// # type Radio<Message> = -/// # iced_widget::Radio<Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Radio<'a, Message> = +/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>; /// # /// # use iced_widget::column; /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -69,9 +67,8 @@ pub use iced_style::radio::{Appearance, StyleSheet}; /// let content = column![a, b, c, all]; /// ``` #[allow(missing_debug_implementations)] -pub struct Radio<Message, Theme = crate::Theme, Renderer = crate::Renderer> +pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { is_selected: bool, @@ -84,20 +81,19 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: Style<'a, Theme>, } -impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { /// The default size of a [`Radio`] button. - pub const DEFAULT_SIZE: f32 = 28.0; + pub const DEFAULT_SIZE: f32 = 16.0; /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 15.0; + pub const DEFAULT_SPACING: f32 = 8.0; /// Creates a new [`Radio`] button. /// @@ -114,6 +110,7 @@ where f: F, ) -> Self where + Theme: DefaultStyle + 'a, V: Eq + Copy, F: FnOnce(V) -> Message, { @@ -128,7 +125,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -178,17 +175,19 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } -impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Radio<Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Radio<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -291,15 +290,18 @@ where viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); + let is_selected = self.is_selected; let mut children = layout.children(); - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) + let status = if is_mouse_over { + Status::Hovered { is_selected } } else { - theme.active(&self.style, self.is_selected) + Status::Active { is_selected } }; + let appearance = (self.style)(theme, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -312,12 +314,12 @@ where bounds, border: Border { radius: (size / 2.0).into(), - width: custom_style.border_width, - color: custom_style.border_color, + width: appearance.border_width, + color: appearance.border_color, }, ..renderer::Quad::default() }, - custom_style.background, + appearance.background, ); if self.is_selected { @@ -329,10 +331,10 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border: Border::with_radius(dot_size / 2.0), + border: Border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, - custom_style.dot_color, + appearance.dot_color, ); } } @@ -346,7 +348,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance { - color: custom_style.text_color, + color: appearance.text_color, }, viewport, ); @@ -354,16 +356,89 @@ where } } -impl<'a, Message, Theme, Renderer> From<Radio<Message, Theme, Renderer>> +impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from( - radio: Radio<Message, Theme, Renderer>, + radio: Radio<'a, Message, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(radio) } } + +/// The possible status of a [`Radio`] button. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Radio`] button can be interacted with. + Active { + /// Indicates whether the [`Radio`] button is currently selected. + is_selected: bool, + }, + /// The [`Radio`] button is being hovered. + Hovered { + /// Indicates whether the [`Radio`] button is currently selected. + is_selected: bool, + }, +} + +/// The appearance of a radio button. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the radio button. + pub background: Background, + /// The [`Color`] of the dot of the radio button. + pub dot_color: Color, + /// The border width of the radio button. + pub border_width: f32, + /// The border [`Color`] of the radio button. + pub border_color: Color, + /// The text [`Color`] of the radio button. + pub text_color: Option<Color>, +} + +/// The style of a [`Radio`] button. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`Radio`] button. +pub trait DefaultStyle { + /// Returns the default style of a [`Radio`] button. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// The default style of a [`Radio`] button. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + background: Color::TRANSPARENT.into(), + dot_color: palette.primary.strong.color, + border_width: 1.0, + border_color: palette.primary.strong.color, + text_color: None, + }; + + match status { + Status::Active { .. } => active, + Status::Hovered { .. } => Appearance { + dot_color: palette.primary.strong.color, + background: palette.primary.weak.color.into(), + ..active + }, + } +} diff --git a/widget/src/row.rs b/widget/src/row.rs index 7f8c335418..47feff9cd9 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -18,6 +18,7 @@ pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { width: Length, height: Length, align_items: Alignment, + clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -27,21 +28,35 @@ where { /// Creates an empty [`Row`]. pub fn new() -> Self { - Row { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align_items: Alignment::Start, - children: Vec::new(), - } + Self::from_vec(Vec::new()) } /// Creates a [`Row`] with the given elements. pub fn with_children( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Self { - children.into_iter().fold(Self::new(), Self::push) + Self::new().extend(children) + } + + /// Creates a [`Row`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Row::width`] or [`Row::height`] accordingly. + pub fn from_vec( + children: Vec<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align_items: Alignment::Start, + clip: false, + children, + } } /// Sets the horizontal spacing _between_ elements. @@ -78,25 +93,47 @@ where self } + /// Sets whether the contents of the [`Row`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + /// Adds an [`Element`] to the [`Row`]. pub fn push( mut self, child: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Self { let child = child.into(); - let size = child.as_widget().size_hint(); + let child_size = child.as_widget().size_hint(); - if size.width.is_fill() { - self.width = Length::Fill; - } - - if size.height.is_fill() { - self.height = Length::Fill; - } + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); self.children.push(child); self } + + /// Adds an element to the [`Row`], if `Some`. + pub fn push_maybe( + self, + child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Row`] with the given children. + pub fn extend( + self, + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } } impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> @@ -229,7 +266,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - if let Some(viewport) = layout.bounds().intersection(viewport) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { for ((child, state), layout) in self .children .iter() @@ -237,7 +274,17 @@ where .zip(layout.children()) { child.as_widget().draw( - state, renderer, theme, style, layout, cursor, &viewport, + state, + renderer, + theme, + style, + layout, + cursor, + if self.clip { + &clipped_viewport + } else { + viewport + }, ); } } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bca345413c..9fa5f74fb1 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,61 +1,60 @@ //! Display a horizontal or vertical rule for dividing content. +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - Border, Element, Layout, Length, Pixels, Rectangle, Size, Widget, + Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget, }; -pub use crate::style::rule::{Appearance, FillMode, StyleSheet}; - /// Display a horizontal or vertical rule for dividing content. #[allow(missing_debug_implementations)] -pub struct Rule<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Rule<'a, Theme = crate::Theme> { width: Length, height: Length, is_horizontal: bool, - style: Theme::Style, + style: Style<'a, Theme>, } -impl<Theme> Rule<Theme> -where - Theme: StyleSheet, -{ +impl<'a, Theme> Rule<'a, Theme> { /// Creates a horizontal [`Rule`] with the given height. - pub fn horizontal(height: impl Into<Pixels>) -> Self { + pub fn horizontal(height: impl Into<Pixels>) -> Self + where + Theme: DefaultStyle + 'a, + { Rule { width: Length::Fill, height: Length::Fixed(height.into().0), is_horizontal: true, - style: Default::default(), + style: Box::new(Theme::default_style), } } /// Creates a vertical [`Rule`] with the given width. - pub fn vertical(width: impl Into<Pixels>) -> Self { + pub fn vertical(width: impl Into<Pixels>) -> Self + where + Theme: DefaultStyle + 'a, + { Rule { width: Length::Fixed(width.into().0), height: Length::Fill, is_horizontal: false, - style: Default::default(), + style: Box::new(Theme::default_style), } } /// Sets the style of the [`Rule`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self { + self.style = Box::new(style); self } } -impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<Theme> +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rule<'a, Theme> where Renderer: crate::core::Renderer, - Theme: StyleSheet, { fn size(&self) -> Size<Length> { Size { @@ -84,34 +83,35 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let style = theme.appearance(&self.style); + let appearance = (self.style)(theme); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) + - (appearance.width as f32 / 2.0)) .round(); - let (offset, line_width) = style.fill_mode.fill(bounds.width); + let (offset, line_width) = appearance.fill_mode.fill(bounds.width); let line_x = bounds.x + offset; Rectangle { x: line_x, y: line_y, width: line_width, - height: style.width as f32, + height: appearance.width as f32, } } else { let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) + - (appearance.width as f32 / 2.0)) .round(); - let (offset, line_height) = style.fill_mode.fill(bounds.height); + let (offset, line_height) = + appearance.fill_mode.fill(bounds.height); let line_y = bounds.y + offset; Rectangle { x: line_x, y: line_y, - width: style.width as f32, + width: appearance.width as f32, height: line_height, } }; @@ -119,22 +119,132 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::with_radius(style.radius), + border: Border::rounded(appearance.radius), ..renderer::Quad::default() }, - style.color, + appearance.color, ); } } -impl<'a, Message, Theme, Renderer> From<Rule<Theme>> +impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { - fn from(rule: Rule<Theme>) -> Element<'a, Message, Theme, Renderer> { + fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(rule) } } + +/// 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: border::Radius, + /// The [`FillMode`] of the rule. + pub fill_mode: FillMode, +} + +/// The fill mode of a rule. +#[derive(Debug, Clone, Copy)] +pub enum FillMode { + /// Fill the whole length of the container. + Full, + /// Fill a percent of the length of the container. The rule + /// will be centered in that container. + /// + /// The range is `[0.0, 100.0]`. + Percent(f32), + /// Uniform offset from each end, length units. + Padded(u16), + /// Different offset on each end of the rule, length units. + /// First = top or left. + AsymmetricPadding(u16, u16), +} + +impl FillMode { + /// Return the starting offset and length of the rule. + /// + /// * `space` - The space to fill. + /// + /// # Returns + /// + /// * (`starting_offset`, `length`) + pub fn fill(&self, space: f32) -> (f32, f32) { + match *self { + FillMode::Full => (0.0, space), + FillMode::Percent(percent) => { + if percent >= 100.0 { + (0.0, space) + } else { + let percent_width = (space * percent / 100.0).round(); + + (((space - percent_width) / 2.0).round(), percent_width) + } + } + FillMode::Padded(padding) => { + if padding == 0 { + (0.0, space) + } else { + let padding = padding as f32; + let mut line_width = space - (padding * 2.0); + if line_width < 0.0 { + line_width = 0.0; + } + + (padding, line_width) + } + } + FillMode::AsymmetricPadding(first_pad, second_pad) => { + let first_pad = first_pad as f32; + let second_pad = second_pad as f32; + let mut line_width = space - first_pad - second_pad; + if line_width < 0.0 { + line_width = 0.0; + } + + (first_pad, line_width) + } + } + } +} + +/// The style of a [`Rule`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>; + +/// The default style of a [`Rule`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Rule`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self) -> Appearance { + *self + } +} + +/// The default styling of a [`Rule`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + color: palette.background.strong.color, + width: 1, + radius: 0.0.into(), + fill_mode: FillMode::Full, + } +} diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 207b2539e1..c03bbb7db1 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,6 @@ //! Navigate an endless amount of content with a scrollbar. +// use crate::container; +use crate::container; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -10,12 +12,11 @@ use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a @@ -27,7 +28,6 @@ pub struct Scrollable< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: crate::core::Renderer, { id: Option<Id>, @@ -36,26 +36,62 @@ pub struct Scrollable< direction: Direction, content: Element<'a, Message, Theme, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, - style: Theme::Style, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { - /// Creates a new [`Scrollable`]. + /// Creates a new vertical [`Scrollable`]. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self + where + Theme: DefaultStyle + 'a, + { + Self::with_direction(content, Direction::default()) + } + + /// Creates a new [`Scrollable`] with the given [`Direction`]. + pub fn with_direction( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + direction: Direction, + ) -> Self + where + Theme: DefaultStyle + 'a, + { + Self::with_direction_and_style(content, direction, Theme::default_style) + } + + /// Creates a new [`Scrollable`] with the given [`Direction`] and style. + pub fn with_direction_and_style( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + direction: Direction, + style: impl Fn(&Theme, Status) -> Appearance + 'a, ) -> Self { + let content = content.into(); + + debug_assert!( + direction.vertical().is_none() + || !content.as_widget().size_hint().height.is_fill(), + "scrollable content must not fill its vertical scrolling axis" + ); + + debug_assert!( + direction.horizontal().is_none() + || !content.as_widget().size_hint().width.is_fill(), + "scrollable content must not fill its horizontal scrolling axis" + ); + Scrollable { id: None, width: Length::Shrink, height: Length::Shrink, - direction: Direction::default(), - content: content.into(), + direction, + content, on_scroll: None, - style: Default::default(), + style: Box::new(style), } } @@ -77,12 +113,6 @@ where self } - /// Sets the [`Direction`] of the [`Scrollable`] . - pub fn direction(mut self, direction: Direction) -> Self { - self.direction = direction; - self - } - /// Sets a function to call when the [`Scrollable`] is scrolled. /// /// The function takes the [`Viewport`] of the [`Scrollable`] @@ -92,8 +122,11 @@ where } /// Sets the style of the [`Scrollable`] . - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } @@ -204,7 +237,6 @@ pub enum Alignment { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Scrollable<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -236,20 +268,29 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - &self.direction, - |renderer, limits| { - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - }, - ) + layout::contained(limits, self.width, self.height, |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if self.direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if self.direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + &child_limits, + ) + }) } fn operate( @@ -299,28 +340,310 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - tree.state.downcast_mut::<State>(), + let state = tree.state.downcast_mut::<State>(); + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let mut event_status = { + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + + state.translation( + self.direction, + bounds, + content_bounds, + ), + ) + } + _ => mouse::Cursor::Unavailable, + }; + + let translation = + state.translation(self.direction, bounds, content_bounds); + + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + content, + cursor, + renderer, + clipboard, + shell, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ) + }; + + if matches!( event, - layout, - cursor, - clipboard, - shell, - self.direction, - &self.on_scroll, - |event, layout, cursor, clipboard, shell, viewport| { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch( + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } ) - }, - ) + ) { + state.scroll_area_touched_at = None; + state.x_scroller_grabbed_at = None; + state.y_scroller_grabbed_at = None; + + return event_status; + } + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = + event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } + + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + // TODO: Configurable speed/friction (?) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; + + state.scroll(delta, self.direction, bounds, content_bounds); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event_status = event::Status::Captured; + } + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => + { + match event { + touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() + else { + return event::Status::Ignored; + }; + + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); + + state.scroll( + delta, + self.direction, + bounds, + content_bounds, + ); + + state.scroll_area_touched_at = + Some(cursor_position); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + _ => {} + } + + event_status = event::Status::Captured; + } + _ => {} + } + + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event_status = event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + event_status = event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + event_status = event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event_status = event::Status::Captured; + } + } + _ => {} + } + } + + event_status } fn draw( @@ -333,26 +656,181 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - tree.state.downcast_ref::<State>(), + let state = tree.state.downcast_ref::<State>(); + + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let cursor_over_scrollable = cursor.position_over(bounds); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let translation = + state.translation(self.direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + let status = if state.y_scroller_grabbed_at.is_some() + || state.x_scroller_grabbed_at.is_some() + { + Status::Dragged { + is_horizontal_scrollbar_dragged: state + .x_scroller_grabbed_at + .is_some(), + is_vertical_scrollbar_dragged: state + .y_scroller_grabbed_at + .is_some(), + } + } else if cursor_over_scrollable.is_some() { + Status::Hovered { + is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, + is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, + } + } else { + Status::Active + }; + + let appearance = (self.style)(theme, status); + + container::draw_background( renderer, - theme, - layout, - cursor, - self.direction, - &self.style, - |renderer, layout, cursor, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - }, + &appearance.container, + layout.bounds(), ); + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(-translation.x, -translation.y), + |renderer| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + content_layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ); + }, + ); + }); + + let draw_scrollbar = + |renderer: &mut Renderer, + style: Scrollbar, + scrollbar: &internals::Scrollbar| { + if scrollbar.bounds.width > 0.0 + && scrollbar.bounds.height > 0.0 + && (style.background.is_some() + || (style.border.color != Color::TRANSPARENT + && style.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border: style.border, + ..renderer::Quad::default() + }, + style.background.unwrap_or(Background::Color( + Color::TRANSPARENT, + )), + ); + } + + if scrollbar.scroller.bounds.width > 0.0 + && scrollbar.scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border.color + != Color::TRANSPARENT + && style.scroller.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border: style.scroller.border, + ..renderer::Quad::default() + }, + style.scroller.color, + ); + } + }; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if let Some(scrollbar) = scrollbars.y { + draw_scrollbar( + renderer, + appearance.vertical_scrollbar, + &scrollbar, + ); + } + + if let Some(scrollbar) = scrollbars.x { + draw_scrollbar( + renderer, + appearance.horizontal_scrollbar, + &scrollbar, + ); + } + + if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) { + let background = + appearance.gap.or(appearance.container.background); + + if let Some(background) = background { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: y.bounds.x, + y: x.bounds.y, + width: y.bounds.width, + height: x.bounds.height, + }, + ..renderer::Quad::default() + }, + background, + ); + } + } + }, + ); + } else { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + content_layout, + cursor, + &Rectangle { + x: bounds.x + translation.x, + y: bounds.y + translation.y, + ..bounds + }, + ); + } } fn mouse_interaction( @@ -363,21 +841,48 @@ where _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref::<State>(), - layout, - cursor, - self.direction, - |layout, cursor, viewport| { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - }, - ) + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { + mouse::Interaction::Idle + } else { + let translation = + state.translation(self.direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + self.content.as_widget().mouse_interaction( + &tree.children[0], + content_layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + renderer, + ) + } } fn overlay<'b>( @@ -411,7 +916,7 @@ impl<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( @@ -463,530 +968,6 @@ pub fn scroll_to<Message: 'static>( Command::widget(operation::scrollable::scroll_to(id.0, offset)) } -/// Computes the layout of a [`Scrollable`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - direction: &Direction, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - layout::contained(limits, width, height, |limits| { - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); - - layout_content(renderer, &child_limits) - }) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] -/// accordingly. -pub fn update<Message>( - state: &mut State, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - direction: Direction, - on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, - update_content: impl FnOnce( - Event, - Layout<'_>, - mouse::Cursor, - &mut dyn Clipboard, - &mut Shell<'_, Message>, - &Rectangle, - ) -> event::Status, -) -> event::Status { - let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(bounds); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - let mut event_status = { - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available( - cursor_position - + state.translation(direction, bounds, content_bounds), - ) - } - _ => mouse::Cursor::Unavailable, - }; - - let translation = state.translation(direction, bounds, content_bounds); - - update_content( - event.clone(), - content, - cursor, - clipboard, - shell, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - }; - - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if cursor_over_scrollable.is_none() { - return event::Status::Ignored; - } - - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { - Vector::new(y, x) - } else { - Vector::new(x, y) - }; - - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; - - state.scroll(delta, direction, bounds, content_bounds); - - notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); - - event_status = event::Status::Captured; - } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { - match event { - touch::Event::FingerPressed { .. } => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll(delta, direction, bounds, content_bounds); - - state.scroll_area_touched_at = Some(cursor_position); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - state.scroll_area_touched_at = None; - } - } - - event_status = event::Status::Captured; - } - _ => {} - } - - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.y_scroller_grabbed_at = None; - - event_status = event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) - { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.x_scroller_grabbed_at = None; - - event_status = event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) - { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } - - event_status -} - -/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. -pub fn mouse_interaction( - state: &State, - layout: Layout<'_>, - cursor: mouse::Cursor, - direction: Direction, - content_interaction: impl FnOnce( - Layout<'_>, - mouse::Cursor, - &Rectangle, - ) -> mouse::Interaction, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(bounds); - - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) - || state.scrollers_grabbed() - { - mouse::Interaction::Idle - } else { - let translation = state.translation(direction, bounds, content_bounds); - - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available(cursor_position + translation) - } - _ => mouse::Cursor::Unavailable, - }; - - content_interaction( - content_layout, - cursor, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - } -} - -/// Draws a [`Scrollable`]. -pub fn draw<Theme, Renderer>( - state: &State, - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - direction: Direction, - style: &Theme::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), -) where - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let cursor_over_scrollable = cursor.position_over(bounds); - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - let translation = state.translation(direction, bounds, content_bounds); - - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available(cursor_position + translation) - } - _ => mouse::Cursor::Unavailable, - }; - - // Draw inner content - if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(-translation.x, -translation.y), - |renderer| { - draw_content( - renderer, - content_layout, - cursor, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ); - }, - ); - }); - - let draw_scrollbar = - |renderer: &mut Renderer, - style: Scrollbar, - scrollbar: &internals::Scrollbar| { - //track - if scrollbar.bounds.width > 0.0 - && scrollbar.bounds.height > 0.0 - && (style.background.is_some() - || (style.border.color != Color::TRANSPARENT - && style.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border: style.border, - ..renderer::Quad::default() - }, - style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - //thumb - if scrollbar.scroller.bounds.width > 0.0 - && scrollbar.scroller.bounds.height > 0.0 - && (style.scroller.color != Color::TRANSPARENT - || (style.scroller.border.color != Color::TRANSPARENT - && style.scroller.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border: style.scroller.border, - ..renderer::Quad::default() - }, - style.scroller.color, - ); - } - }; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - //draw y scrollbar - if let Some(scrollbar) = scrollbars.y { - let style = if state.y_scroller_grabbed_at.is_some() { - theme.dragging(style) - } else if cursor_over_scrollable.is_some() { - theme.hovered(style, mouse_over_y_scrollbar) - } else { - theme.active(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - - //draw x scrollbar - if let Some(scrollbar) = scrollbars.x { - let style = if state.x_scroller_grabbed_at.is_some() { - theme.dragging_horizontal(style) - } else if cursor_over_scrollable.is_some() { - theme.hovered_horizontal(style, mouse_over_x_scrollbar) - } else { - theme.active_horizontal(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - }, - ); - } else { - draw_content( - renderer, - content_layout, - cursor, - &Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..bounds - }, - ); - } -} - fn notify_on_scroll<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, @@ -1034,9 +1015,8 @@ fn notify_on_scroll<Message>( } } -/// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] -pub struct State { +struct State { scroll_area_touched_at: Option<Point>, offset_y: Offset, y_scroller_grabbed_at: Option<f32>, @@ -1566,3 +1546,155 @@ pub(super) mod internals { pub bounds: Rectangle, } } + +/// The possible status of a [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Scrollable`] can be interacted with. + Active, + /// The [`Scrollable`] is being hovered. + Hovered { + /// Indicates if the horizontal scrollbar is being hovered. + is_horizontal_scrollbar_hovered: bool, + /// Indicates if the vertical scrollbar is being hovered. + is_vertical_scrollbar_hovered: bool, + }, + /// The [`Scrollable`] is being dragged. + Dragged { + /// Indicates if the horizontal scrollbar is being dragged. + is_horizontal_scrollbar_dragged: bool, + /// Indicates if the vertical scrollbar is being dragged. + is_vertical_scrollbar_dragged: bool, + }, +} + +/// The appearance of a scrolable. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`container::Appearance`] of a scrollable. + pub container: container::Appearance, + /// The vertical [`Scrollbar`] appearance. + pub vertical_scrollbar: Scrollbar, + /// The horizontal [`Scrollbar`] appearance. + pub horizontal_scrollbar: Scrollbar, + /// The [`Background`] of the gap between a horizontal and vertical scrollbar. + pub gap: Option<Background>, +} + +/// The appearance of the scrollbar of a scrollable. +#[derive(Debug, Clone, Copy)] +pub struct Scrollbar { + /// The [`Background`] of a scrollbar. + pub background: Option<Background>, + /// The [`Border`] of a scrollbar. + pub border: Border, + /// The appearance of the [`Scroller`] of a scrollbar. + pub scroller: Scroller, +} + +/// The appearance of the scroller of a scrollable. +#[derive(Debug, Clone, Copy)] +pub struct Scroller { + /// The [`Color`] of the scroller. + pub color: Color, + /// The [`Border`] of the scroller. + pub border: Border, +} + +/// The style of a [`Scrollable`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`Scrollable`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Scrollable`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// The default style of a [`Scrollable`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let scrollbar = Scrollbar { + background: Some(palette.background.weak.color.into()), + border: Border::rounded(2), + scroller: Scroller { + color: palette.background.strong.color, + border: Border::rounded(2), + }, + }; + + match status { + Status::Active => Appearance { + container: container::Appearance::default(), + vertical_scrollbar: scrollbar, + horizontal_scrollbar: scrollbar, + gap: None, + }, + Status::Hovered { + is_horizontal_scrollbar_hovered, + is_vertical_scrollbar_hovered, + } => { + let hovered_scrollbar = Scrollbar { + scroller: Scroller { + color: palette.primary.strong.color, + ..scrollbar.scroller + }, + ..scrollbar + }; + + Appearance { + container: container::Appearance::default(), + vertical_scrollbar: if is_vertical_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + horizontal_scrollbar: if is_horizontal_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + Status::Dragged { + is_horizontal_scrollbar_dragged, + is_vertical_scrollbar_dragged, + } => { + let dragged_scrollbar = Scrollbar { + scroller: Scroller { + color: palette.primary.base.color, + ..scrollbar.scroller + }, + ..scrollbar + }; + + Appearance { + container: container::Appearance::default(), + vertical_scrollbar: if is_vertical_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + horizontal_scrollbar: if is_horizontal_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + } +} diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 65bc1772eb..d3b46a98c1 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,6 +1,5 @@ //! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. +use crate::core::border; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -10,16 +9,12 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Size, Widget, + Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; -pub use iced_style::slider::{ - Appearance, Handle, HandleShape, Rail, StyleSheet, -}; - /// An horizontal bar and a handle that selects a single value from a range of /// values. /// @@ -30,8 +25,7 @@ pub use iced_style::slider::{ /// /// # Example /// ```no_run -/// # type Slider<'a, T, Message> = -/// # iced_widget::Slider<'a, Message, T, iced_widget::style::Theme>; +/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>; /// # /// #[derive(Clone)] /// pub enum Message { @@ -45,10 +39,7 @@ pub use iced_style::slider::{ /// /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Slider<'a, T, Message, Theme = crate::Theme> { range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -58,17 +49,16 @@ where on_release: Option<Message>, width: Length, height: f32, - style: Theme::Style, + style: Style<'a, Theme>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + From<u8> + PartialOrd, Message: Clone, - Theme: StyleSheet, { /// The default height of a [`Slider`]. - pub const DEFAULT_HEIGHT: f32 = 22.0; + pub const DEFAULT_HEIGHT: f32 = 16.0; /// Creates a new [`Slider`]. /// @@ -80,6 +70,7 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where + Theme: DefaultStyle + 'a, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -104,7 +95,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -140,8 +131,11 @@ where } /// Sets the style of the [`Slider`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } @@ -165,7 +159,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -173,7 +166,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -203,20 +196,143 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::<State>(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) + let state = tree.state.downcast_mut::<State>(); + + let is_dragging = state.is_dragging; + let current_value = self.value; + + let locate = |cursor_position: Point| -> Option<T> { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + Some(*self.range.start()) + } else if cursor_position.x >= bounds.x + bounds.width { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + 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; + + T::from_f64(value) + }; + + new_value + }; + + let increment = |value: T| -> Option<T> { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); + + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } + + T::from_f64(new_value) + }; + + let decrement = |value: T| -> Option<T> { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); + + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } + + T::from_f64(new_value) + }; + + let change = |new_value: T| { + 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 let Some(cursor_position) = + cursor.position_over(layout.bounds()) + { + if state.keyboard_modifiers.command() { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(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) = self.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 { + let _ = cursor.position().and_then(locate).map(change); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -229,15 +345,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = (self.style)( theme, - &self.style, + if state.is_dragging { + Status::Dragged + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }, + ); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), 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 offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + let rail_y = bounds.y + bounds.height / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border: Border::rounded(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset - handle_width / 2.0, + height: style.rail.width, + }, + border: Border::rounded(style.rail.border_radius), + ..renderer::Quad::default() + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border: Border { + radius: handle_border_radius, + width: style.handle.border_width, + color: style.handle.border_color, + }, + ..renderer::Quad::default() + }, + style.handle.color, ); } @@ -249,7 +442,17 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } } } @@ -258,7 +461,7 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>> where T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { fn from( @@ -268,290 +471,126 @@ where } } -/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] -/// accordingly. -pub fn update<Message, T>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - default: Option<T>, - range: &RangeInclusive<T>, - step: T, - shift_step: Option<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 current_value = *value; - - let locate = |cursor_position: Point| -> Option<T> { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - Some(*range.start()) - } else if cursor_position.x >= bounds.x + bounds.width { - Some(*range.end()) - } else { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let start = (*range.start()).into(); - let end = (*range.end()).into(); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +struct State { + is_dragging: bool, + keyboard_modifiers: keyboard::Modifiers, +} - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); +/// The possible status of a [`Slider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Slider`] can be interacted with. + Active, + /// The [`Slider`] is being hovered. + Hovered, + /// The [`Slider`] is being dragged. + Dragged, +} - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; +/// The appearance of a slider. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The colors of the rail of the slider. + pub rail: Rail, + /// The appearance of the [`Handle`] of the slider. + pub handle: Handle, +} - T::from_f64(value) +impl Appearance { + /// Changes the [`HandleShape`] of the [`Appearance`] to a circle + /// with the given radius. + pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self { + self.handle.shape = HandleShape::Circle { + radius: radius.into().0, }; + self + } +} - new_value - }; - - let increment = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*range.end()).into() { - return Some(*range.end()); - } - - T::from_f64(new_value) - }; - - let decrement = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); - - if new_value < (*range.start()).into() { - return Some(*range.start()); - } - - T::from_f64(new_value) - }; - - let change = |new_value: T| { - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); - - *value = new_value; - } - }; +/// The appearance of a slider rail +#[derive(Debug, Clone, Copy)] +pub struct Rail { + /// The colors of the rail of the slider. + pub colors: (Color, Color), + /// The width of the stroke of a slider rail. + pub width: f32, + /// The border radius of the corners of the rail. + pub border_radius: border::Radius, +} - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.command() { - let _ = default.map(change); - state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; - } +/// The appearance of the handle of a slider. +#[derive(Debug, Clone, Copy)] +pub struct Handle { + /// The shape of the handle. + pub shape: HandleShape, + /// The [`Color`] of the handle. + pub color: Color, + /// The border width of the handle. + pub border_width: f32, + /// The border [`Color`] of the handle. + pub border_color: Color, +} - 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; +/// The shape of the handle of a slider. +#[derive(Debug, Clone, Copy)] +pub enum HandleShape { + /// A circular handle. + Circle { + /// The radius of the circle. + radius: f32, + }, + /// A rectangular shape. + Rectangle { + /// The width of the rectangle. + width: u16, + /// The border radius of the corners of the rectangle. + border_radius: border::Radius, + }, +} - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); +/// The style of a [`Slider`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { - match key { - Key::Named(key::Named::ArrowUp) => { - let _ = increment(current_value).map(change); - } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } +/// The default style of a [`Slider`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Slider`]. + fn default_style(&self, status: Status) -> Appearance; +} - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) } - - event::Status::Ignored } -/// Draws a [`Slider`]. -pub fn draw<T, Theme, Renderer>( - renderer: &mut Renderer, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, - value: T, - range: &RangeInclusive<T>, - theme: &Theme, - style: &Theme::Style, -) where - T: Into<f64> + Copy, - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let style = if state.is_dragging { - theme.dragging(style) - } else if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - let (handle_width, handle_height, handle_border_radius) = - match style.handle.shape { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) - } - 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(); +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} - (start.into() as f32, end.into() as f32) - }; +/// The default style of a [`Slider`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); - let offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) + let color = match status { + Status::Active => palette.primary.strong.color, + Status::Hovered => palette.primary.base.color, + Status::Dragged => palette.primary.strong.color, }; - let rail_y = bounds.y + bounds.height / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y - style.rail.width / 2.0, - width: offset + handle_width / 2.0, - height: style.rail.width, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() + Appearance { + rail: Rail { + colors: (color, palette.secondary.base.color), + width: 4.0, + border_radius: 2.0.into(), }, - style.rail.colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + offset + handle_width / 2.0, - y: rail_y - style.rail.width / 2.0, - width: bounds.width - offset - handle_width / 2.0, - height: style.rail.width, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + offset, - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, - }, - ..renderer::Quad::default() + handle: Handle { + shape: HandleShape::Circle { radius: 7.0 }, + color, + border_color: Color::TRANSPARENT, + border_width: 0.0, }, - style.handle.color, - ); -} - -/// Computes the current [`mouse::Interaction`] of a [`Slider`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - 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, - keyboard_modifiers: keyboard::Modifiers, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() } } diff --git a/widget/src/space.rs b/widget/src/space.rs index aeec91f99c..35bb30c4d2 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -39,6 +39,18 @@ impl Space { height: height.into(), } } + + /// Sets the width of the [`Space`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Space`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } } impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Space diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 12ef3d925c..1ac07ade3e 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,13 +5,13 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector, + Widget, }; use std::path::PathBuf; -pub use crate::style::svg::{Appearance, StyleSheet}; -pub use svg::Handle; +pub use crate::core::svg::Handle; /// A vector graphics image. /// @@ -20,36 +20,36 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Svg<'a, Theme = crate::Theme> { handle: Handle, width: Length, height: Length, content_fit: ContentFit, - style: <Theme as StyleSheet>::Style, + style: Style<'a, Theme>, } -impl<Theme> Svg<Theme> -where - Theme: StyleSheet, -{ +impl<'a, Theme> Svg<'a, Theme> { /// Creates a new [`Svg`] from the given [`Handle`]. - pub fn new(handle: impl Into<Handle>) -> Self { + pub fn new(handle: impl Into<Handle>) -> Self + where + Theme: DefaultStyle + 'a, + { Svg { handle: handle.into(), width: Length::Fill, height: Length::Shrink, content_fit: ContentFit::Contain, - style: Default::default(), + style: Box::new(Theme::default_style), } } /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. #[must_use] - pub fn from_path(path: impl Into<PathBuf>) -> Self { + pub fn from_path(path: impl Into<PathBuf>) -> Self + where + Theme: DefaultStyle + 'a, + { Self::new(Handle::from_path(path)) } @@ -80,15 +80,18 @@ where /// Sets the style variant of this [`Svg`]. #[must_use] - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } -impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<Theme> +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Svg<'a, Theme> where - Theme: iced_style::svg::StyleSheet, Renderer: svg::Renderer, { fn size(&self) -> Size<Length> { @@ -158,12 +161,14 @@ where ..bounds }; - let appearance = if is_mouse_over { - theme.hovered(&self.style) + let status = if is_mouse_over { + Status::Hovered } else { - theme.appearance(&self.style) + Status::Idle }; + let appearance = (self.style)(theme, status); + renderer.draw( self.handle.clone(), appearance.color, @@ -181,13 +186,54 @@ where } } -impl<'a, Message, Theme, Renderer> From<Svg<Theme>> +impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: iced_style::svg::StyleSheet + 'a, + Theme: 'a, Renderer: svg::Renderer + 'a, { - fn from(icon: Svg<Theme>) -> Element<'a, Message, Theme, Renderer> { + fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> { Element::new(icon) } } + +/// The possible status of an [`Svg`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Svg`] is idle. + Idle, + /// The [`Svg`] is being hovered. + Hovered, +} + +/// The appearance of an [`Svg`]. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Appearance { + /// The [`Color`] filter of an [`Svg`]. + /// + /// Useful for coloring a symbolic icon. + /// + /// `None` keeps the original color. + pub color: Option<Color>, +} + +/// The style of an [`Svg`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of an [`Svg`]. +pub trait DefaultStyle { + /// Returns the default style of an [`Svg`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, _status: Status) -> Appearance { + Appearance::default() + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index cbcab1ebb4..5b8f6a1b72 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,4 +1,5 @@ //! Display a multi-line text input for text editing. +use crate::core::clipboard::{self, Clipboard}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key; @@ -10,7 +11,8 @@ use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, + Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, + Shell, Size, Theme, Vector, }; use std::cell::RefCell; @@ -18,7 +20,6 @@ use std::fmt; use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. @@ -31,7 +32,6 @@ pub struct TextEditor< Renderer = crate::Renderer, > where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { content: &'a Content<Renderer>, @@ -41,7 +41,7 @@ pub struct TextEditor< width: Length, height: Length, padding: Padding, - style: Theme::Style, + style: Style<'a, Theme>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -53,11 +53,13 @@ pub struct TextEditor< impl<'a, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// Creates new [`TextEditor`] with the given [`Content`]. - pub fn new(content: &'a Content<Renderer>) -> Self { + pub fn new(content: &'a Content<Renderer>) -> Self + where + Theme: DefaultStyle + 'a, + { Self { content, font: None, @@ -66,7 +68,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), - style: Default::default(), + style: Box::new(Theme::default_style), on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -80,7 +82,6 @@ impl<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { /// Sets the height of the [`TextEditor`]. @@ -141,8 +142,11 @@ where } /// Sets the style of the [`TextEditor`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } @@ -305,7 +309,6 @@ impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> widget::tree::Tag { @@ -448,17 +451,19 @@ where } Update::Copy => { if let Some(selection) = self.content.selection() { - clipboard.write(selection); + clipboard.write(clipboard::Kind::Standard, selection); } } Update::Cut => { if let Some(selection) = self.content.selection() { - clipboard.write(selection.clone()); + clipboard.write(clipboard::Kind::Standard, selection); shell.publish(on_edit(Action::Edit(Edit::Delete))); } } Update::Paste => { - if let Some(contents) = clipboard.read() { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { shell.publish(on_edit(Action::Edit(Edit::Paste( Arc::new(contents), )))); @@ -493,16 +498,18 @@ where let is_disabled = self.on_edit.is_none(); let is_mouse_over = cursor.is_over(bounds); - let appearance = if is_disabled { - theme.disabled(&self.style) + let status = if is_disabled { + Status::Disabled } else if state.is_focused { - theme.focused(&self.style) + Status::Focused } else if is_mouse_over { - theme.hovered(&self.style) + Status::Hovered } else { - theme.active(&self.style) + Status::Active }; + let appearance = (self.style)(theme, status); + renderer.fill_quad( renderer::Quad { bounds, @@ -534,7 +541,7 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: position.x, + x: position.x.floor(), y: position.y, width: 1.0, height: self @@ -548,7 +555,7 @@ where }, ..renderer::Quad::default() }, - theme.value_color(&self.style), + appearance.value, ); } } @@ -561,7 +568,7 @@ where bounds: range, ..renderer::Quad::default() }, - theme.selection_color(&self.style), + appearance.selection, ); } } @@ -597,7 +604,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer, { fn from( @@ -683,60 +690,63 @@ impl Update { text, .. } if state.is_focused => { - if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { - let motion = if platform::is_jump_modifier_pressed( - modifiers, - ) { - motion.widen() - } else { - motion - }; - - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); - } - } - match key.as_ref() { keyboard::Key::Named(key::Named::Enter) => { - edit(Edit::Enter) + return edit(Edit::Enter); } keyboard::Key::Named(key::Named::Backspace) => { - edit(Edit::Backspace) + return edit(Edit::Backspace); } keyboard::Key::Named(key::Named::Delete) => { - edit(Edit::Delete) + return edit(Edit::Delete); } keyboard::Key::Named(key::Named::Escape) => { - Some(Self::Unfocus) + return Some(Self::Unfocus); } keyboard::Key::Character("c") if modifiers.command() => { - Some(Self::Copy) + return Some(Self::Copy); } keyboard::Key::Character("x") if modifiers.command() => { - Some(Self::Cut) + return Some(Self::Cut); } keyboard::Key::Character("v") if modifiers.command() && !modifiers.alt() => { - Some(Self::Paste) + return Some(Self::Paste); } - _ => { - let text = text?; + _ => {} + } - edit(Edit::Insert( - text.chars().next().unwrap_or_default(), - )) + if let Some(text) = text { + if let Some(c) = text.chars().find(|c| !c.is_control()) + { + return edit(Edit::Insert(c)); } } + + if let keyboard::Key::Named(named_key) = key.as_ref() { + if let Some(motion) = motion(named_key) { + let motion = if platform::is_jump_modifier_pressed( + modifiers, + ) { + motion.widen() + } else { + motion + }; + + return action(if modifiers.shift() { + Action::Select(motion) + } else { + Action::Move(motion) + }); + } + } + + None } _ => None, }, @@ -770,3 +780,95 @@ mod platform { } } } + +/// The possible status of a [`TextEditor`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`TextEditor`] can be interacted with. + Active, + /// The [`TextEditor`] is being hovered. + Hovered, + /// The [`TextEditor`] is focused. + Focused, + /// The [`TextEditor`] cannot be interacted with. + Disabled, +} + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The [`Border`] of the text input. + pub border: Border, + /// The [`Color`] of the icon of the text input. + pub icon: Color, + /// The [`Color`] of the placeholder of the text input. + pub placeholder: Color, + /// The [`Color`] of the value of the text input. + pub value: Color, + /// The [`Color`] of the selection of the text input. + pub selection: Color, +} + +/// The style of a [`TextEditor`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`TextEditor`]. +pub trait DefaultStyle { + /// Returns the default style of a [`TextEditor`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// The default style of a [`TextEditor`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + background: Background::Color(palette.background.base.color), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.background.strong.color, + }, + icon: palette.background.weak.text, + placeholder: palette.background.strong.color, + value: palette.background.base.text, + selection: palette.primary.weak.color, + }; + + match status { + Status::Active => active, + Status::Hovered => Appearance { + border: Border { + color: palette.background.base.text, + ..active.border + }, + ..active + }, + Status::Focused => Appearance { + border: Border { + color: palette.primary.strong.color, + ..active.border + }, + ..active + }, + Status::Disabled => Appearance { + background: Background::Color(palette.background.weak.color), + value: active.placeholder, + ..active + }, + } +} diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 0a7ed01493..b161ec74f9 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -12,6 +12,7 @@ pub use value::Value; use editor::Editor; use crate::core::alignment; +use crate::core::clipboard::{self, Clipboard}; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key; @@ -26,19 +27,16 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use iced_style::text_input::{Appearance, StyleSheet}; - /// A field that can be filled with text. /// /// # Example /// ```no_run -/// # pub type TextInput<'a, Message> = -/// # iced_widget::TextInput<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # pub type TextInput<'a, Message> = iced_widget::TextInput<'a, Message>; /// # /// #[derive(Debug, Clone)] /// enum Message { @@ -62,7 +60,6 @@ pub struct TextInput< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { id: Option<Id>, @@ -78,7 +75,7 @@ pub struct TextInput< on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, icon: Option<Icon<Renderer::Font>>, - style: Theme::Style, + style: Style<'a, Theme>, } /// The default [`Padding`] of a [`TextInput`]. @@ -87,15 +84,24 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0); impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value - pub fn new(placeholder: &str, value: &str) -> Self { + /// Creates a new [`TextInput`] with the given placeholder and + /// its current value. + pub fn new(placeholder: &str, value: &str) -> Self + where + Theme: DefaultStyle + 'a, + { + Self::with_style(placeholder, value, Theme::default_style) + } + + /// Creates a new [`TextInput`] with the given placeholder, + /// its current value, and its style. + pub fn with_style( + placeholder: &str, + value: &str, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { TextInput { id: None, placeholder: String::from(placeholder), @@ -110,7 +116,7 @@ where on_paste: None, on_submit: None, icon: None, - style: Default::default(), + style: Box::new(style), } } @@ -121,8 +127,8 @@ where } /// Converts the [`TextInput`] into a secure password input. - pub fn password(mut self) -> Self { - self.is_secure = true; + pub fn secure(mut self, is_secure: bool) -> Self { + self.is_secure = is_secure; self } @@ -197,8 +203,11 @@ where } /// Sets the style of the [`TextInput`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } @@ -212,20 +221,90 @@ where limits: &layout::Limits, value: Option<&Value>, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - &self.placeholder, - self.is_secure, - ) + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let text_size = self.size.unwrap_or_else(|| renderer.default_size()); + let padding = self.padding.fit(Size::ZERO, limits.max()); + let height = self.line_height.to_absolute(text_size); + + let limits = limits.width(self.width).shrink(padding); + let text_bounds = limits.resolve(self.width, height, Size::ZERO); + + let placeholder_text = Text { + font, + line_height: self.line_height, + content: &self.placeholder, + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.placeholder.update(placeholder_text); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); + + if let Some(icon) = &self.icon { + let icon_text = Text { + line_height: self.line_height, + content: &icon.code_point.to_string(), + font: icon.font, + size: icon.size.unwrap_or_else(|| renderer.default_size()), + bounds: Size::new(f32::INFINITY, text_bounds.height), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.icon.update(icon_text); + + let icon_width = state.icon.min_width(); + + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( + padding.left + icon_width + icon.spacing, + padding.top, + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( + padding.left + text_bounds.width - icon_width, + padding.top, + ), + ), + }; + + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + + layout::Node::with_children( + text_bounds.expand(padding), + vec![text_node, icon_node], + ) + } else { + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(text_bounds.expand(padding), vec![text]) + } } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -242,19 +321,174 @@ where value: Option<&Value>, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + let is_disabled = self.on_input.is_none(); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let bounds = layout.bounds(); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + let is_mouse_over = cursor.is_over(bounds); + + let status = if is_disabled { + Status::Disabled + } else if state.is_focused() { + Status::Focused + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }; + + let appearance = (self.style)(theme, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.border, + ..renderer::Quad::default() + }, + appearance.background, ); + + if self.icon.is_some() { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_paragraph( + &state.icon, + icon_layout.bounds().center(), + appearance.icon, + *viewport, + ); + } + + let text = value.to_string(); + + let (cursor, offset) = if let Some(focus) = state + .is_focused + .as_ref() + .filter(|focus| focus.is_window_focused) + { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + position, + ); + + let is_cursor_visible = ((focus.now - focus.updated_at) + .as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; + + let cursor = if is_cursor_visible { + Some(( + renderer::Quad { + bounds: Rectangle { + x: (text_bounds.x + text_value_width) + .floor(), + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + ..renderer::Quad::default() + }, + appearance.value, + )) + } else { + None + }; + + (cursor, 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( + &state.value, + text_bounds, + left, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + right, + ); + + 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, + }, + ..renderer::Quad::default() + }, + appearance.selection, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let draw = |renderer: &mut Renderer, viewport| { + if let Some((cursor, color)) = cursor { + renderer.with_translation( + Vector::new(-offset, 0.0), + |renderer| { + renderer.fill_quad(cursor, color); + }, + ); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), + if text.is_empty() { + appearance.placeholder + } else { + appearance.value + }, + viewport, + ); + }; + + if cursor.is_some() { + renderer + .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); + } else { + draw(renderer, text_bounds); + } } } @@ -262,7 +496,6 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -298,20 +531,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - &self.value, - &self.placeholder, - self.is_secure, - ) + self.layout(tree, renderer, limits, None) } fn operate( @@ -338,23 +558,468 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - self.line_height, - self.font, - self.is_secure, - self.on_input.as_deref(), - self.on_paste.as_deref(), - &self.on_submit, - || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - ) + let update_cache = |state, value| { + replace_paragraph( + renderer, + state, + layout, + value, + self.font, + self.size, + self.line_height, + ); + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state::<Renderer>(tree); + + let click_position = if self.on_input.is_some() { + cursor.position_over(layout.bounds()) + } else { + None + }; + + state.is_focused = if click_position.is_some() { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + is_window_focused: true, + }) + }) + } else { + None + }; + + if let Some(cursor_position) = click_position { + 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 self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + } else { + None + } + .unwrap_or(0); + + if state.keyboard_modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + position, + ); + } else { + state.cursor.move_to(position); + } + state.is_dragging = true; + } + click::Kind::Double => { + if self.is_secure { + state.cursor.select_all(&self.value); + } else { + let position = find_cursor_position( + text_layout.bounds(), + &self.value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + self.value.previous_start_of_word(position), + self.value.next_end_of_word(position), + ); + } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(&self.value); + state.is_dragging = false; + } + } + + 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 { .. }) => { + state::<Renderer>(tree).is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state::<Renderer>(tree); + + if 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() + }; + + let position = find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { + key, text, .. + }) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + + let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); + + match key.as_ref() { + keyboard::Key::Character("c") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + return event::Status::Captured; + } + keyboard::Key::Character("x") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("v") + if state.keyboard_modifiers.command() + && !state.keyboard_modifiers.alt() => + { + let content = match state.is_pasting.take() { + Some(content) => content, + None => { + let content: String = clipboard + .read(clipboard::Kind::Standard) + .unwrap_or_default() + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.paste(content.clone()); + + let message = if let Some(paste) = &self.on_paste { + (paste)(editor.contents()) + } else { + (on_input)(editor.contents()) + }; + shell.publish(message); + + state.is_pasting = Some(content); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("a") + if state.keyboard_modifiers.command() => + { + state.cursor.select_all(&self.value); + + return event::Status::Captured; + } + _ => {} + } + + if let Some(text) = text { + state.is_pasting = None; + + if let Some(c) = + text.chars().next().filter(|c| !c.is_control()) + { + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + if let Some(on_submit) = self.on_submit.clone() { + shell.publish(on_submit); + } + } + keyboard::Key::Named(key::Named::Backspace) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range(0, cursor_pos); + } else { + state + .cursor + .select_left_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.backspace(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::Delete) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range( + cursor_pos, + self.value.len(), + ); + } else { + state + .cursor + .select_right_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::ArrowLeft) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_left_by_words(&self.value); + } else { + state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_left(&self.value); + } else { + state.cursor.move_left(&self.value); + } + } + keyboard::Key::Named(key::Named::ArrowRight) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_right_by_words(&self.value); + } else { + state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_right(&self.value); + } else { + state.cursor.move_right(&self.value); + } + } + keyboard::Key::Named(key::Named::Home) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::End) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } + keyboard::Key::Named(key::Named::Escape) => { + state.is_focused = None; + state.is_dragging = false; + state.is_pasting = None; + + state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { + let state = state::<Renderer>(tree); + + if state.is_focused.is_some() { + match key.as_ref() { + keyboard::Key::Character("v") => { + state.is_pasting = None; + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + + state.is_pasting = None; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state::<Renderer>(tree); + + state.keyboard_modifiers = modifiers; + } + Event::Window(_, window::Event::Unfocused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = false; + } + } + Event::Window(_, window::Event::Focused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Window(_, window::Event::RedrawRequested(now)) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + if focus.is_window_focused { + focus.now = now; + + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis( + millis_until_redraw as u64, + ), + )); + } + } + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -367,19 +1032,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - &self.value, - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, - ); + self.draw(tree, renderer, theme, layout, cursor, None, viewport); } fn mouse_interaction( @@ -390,7 +1043,15 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, self.on_input.is_none()) + if cursor.is_over(layout.bounds()) { + if self.on_input.is_none() { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } } } @@ -398,7 +1059,7 @@ impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -487,766 +1148,26 @@ pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { Command::widget(operation::text_input::select_all(id.0)) } -/// Computes the layout of a [`TextInput`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - size: Option<Pixels>, - font: Option<Renderer::Font>, - line_height: text::LineHeight, - icon: Option<&Icon<Renderer::Font>>, - state: &mut State<Renderer::Paragraph>, - value: &Value, - placeholder: &str, - is_secure: bool, -) -> layout::Node -where - Renderer: text::Renderer, -{ - let font = font.unwrap_or_else(|| renderer.default_font()); - let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let height = line_height.to_absolute(text_size); - - let limits = limits.width(width).shrink(padding); - let text_bounds = limits.resolve(width, height, Size::ZERO); - - let placeholder_text = Text { - font, - line_height, - content: placeholder, - bounds: Size::new(f32::INFINITY, text_bounds.height), - size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.placeholder.update(placeholder_text); +/// The state of a [`TextInput`]. +#[derive(Debug, Default, Clone)] +pub struct State<P: text::Paragraph> { + value: P, + placeholder: P, + icon: P, + is_focused: Option<Focus>, + is_dragging: bool, + is_pasting: Option<Value>, + last_click: Option<mouse::Click>, + cursor: Cursor, + keyboard_modifiers: keyboard::Modifiers, + // TODO: Add stateful horizontal scrolling offset +} - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - state.value.update(Text { - content: &value.to_string(), - ..placeholder_text - }); - - if let Some(icon) = icon { - let icon_text = Text { - line_height, - content: &icon.code_point.to_string(), - font: icon.font, - size: icon.size.unwrap_or_else(|| renderer.default_size()), - bounds: Size::new(f32::INFINITY, text_bounds.height), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.icon.update(icon_text); - - let icon_width = state.icon.min_width(); - - let (text_position, icon_position) = match icon.side { - Side::Left => ( - Point::new( - padding.left + icon_width + icon.spacing, - padding.top, - ), - Point::new(padding.left, padding.top), - ), - Side::Right => ( - Point::new(padding.left, padding.top), - Point::new( - padding.left + text_bounds.width - icon_width, - padding.top, - ), - ), - }; - - let text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ) - .move_to(text_position); - - let icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)) - .move_to(icon_position); - - layout::Node::with_children( - text_bounds.expand(padding), - vec![text_node, icon_node], - ) - } else { - let text = layout::Node::new(text_bounds) - .move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(text_bounds.expand(padding), vec![text]) - } -} - -/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] -/// accordingly. -pub fn update<'a, Message, Renderer>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - value: &mut Value, - size: Option<Pixels>, - line_height: text::LineHeight, - font: Option<Renderer::Font>, - is_secure: bool, - on_input: Option<&dyn Fn(String) -> Message>, - on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: &Option<Message>, - state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>, -) -> event::Status -where - Message: Clone, - Renderer: text::Renderer, -{ - let update_cache = |state, value| { - replace_paragraph( - renderer, - state, - layout, - value, - font, - size, - line_height, - ); - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let click_position = if on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; - - state.is_focused = if click_position.is_some() { - state.is_focused.or_else(|| { - let now = Instant::now(); - - Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }) - }) - } else { - None - }; - - if let Some(cursor_position) = click_position { - 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 { - value.clone() - }; - - find_cursor_position( - text_layout.bounds(), - &value, - state, - target, - ) - } else { - None - } - .unwrap_or(0); - - if state.keyboard_modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - position, - ); - } else { - state.cursor.move_to(position); - } - state.is_dragging = true; - } - click::Kind::Double => { - if is_secure { - state.cursor.select_all(value); - } else { - let position = find_cursor_position( - text_layout.bounds(), - 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; - } - } - - 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 { .. }) => { - state().is_dragging = false; - } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - let state = state(); - - if state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - let position = find_cursor_position( - text_layout.bounds(), - &value, - state, - target, - ) - .unwrap_or(0); - - state - .cursor - .select_range(state.cursor.start(&value), position); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); - - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - if let Some(on_submit) = on_submit.clone() { - shell.publish(on_submit); - } - } - keyboard::Key::Named(key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state.cursor.select_range(0, cursor_pos); - } else { - state.cursor.select_left_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.backspace(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state - .cursor - .select_range(cursor_pos, value.len()); - } else { - state.cursor.select_right_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_left_by_words(value); - } else { - state.cursor.move_left_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_left(value); - } else { - state.cursor.move_left(value); - } - } - keyboard::Key::Named(key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_right_by_words(value); - } else { - state.cursor.move_right_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_right(value); - } else { - state.cursor.move_right(value); - } - } - keyboard::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state - .cursor - .select_range(state.cursor.start(value), 0); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - value.len(), - ); - } else { - state.cursor.move_to(value.len()); - } - } - keyboard::Key::Character("c") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard - .write(value.select(start, end).to_string()); - } - } - keyboard::Key::Character("x") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard - .write(value.select(start, end).to_string()); - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Character("v") - if state.keyboard_modifiers.command() - && !state.keyboard_modifiers.alt() => - { - let content = match state.is_pasting.take() { - Some(content) => content, - None => { - let content: String = clipboard - .read() - .unwrap_or_default() - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - } - }; - - let mut editor = Editor::new(value, &mut state.cursor); - - editor.paste(content.clone()); - - let message = if let Some(paste) = &on_paste { - (paste)(editor.contents()) - } else { - (on_input)(editor.contents()) - }; - shell.publish(message); - - state.is_pasting = Some(content); - - update_cache(state, value); - } - keyboard::Key::Character("a") - if state.keyboard_modifiers.command() => - { - state.cursor.select_all(value); - } - keyboard::Key::Named(key::Named::Escape) => { - state.is_focused = None; - state.is_dragging = false; - state.is_pasting = None; - - state.keyboard_modifiers = - keyboard::Modifiers::default(); - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => { - if let Some(text) = text { - state.is_pasting = None; - - let c = text.chars().next().unwrap_or_default(); - - if !c.is_control() { - let mut editor = - Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - } - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { - let state = state(); - - if state.is_focused.is_some() { - match key.as_ref() { - keyboard::Key::Character("v") => { - state.is_pasting = None; - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } else { - state.is_pasting = None; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - } - Event::Window(_, window::Event::Unfocused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = false; - } - } - Event::Window(_, window::Event::Focused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = true; - focus.updated_at = Instant::now(); - - shell.request_redraw(window::RedrawRequest::NextFrame); - } - } - Event::Window(_, window::Event::RedrawRequested(now)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - if focus.is_window_focused { - focus.now = now; - - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() - % CURSOR_BLINK_INTERVAL_MILLIS; - - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(millis_until_redraw as u64), - )); - } - } - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws the [`TextInput`] with the given [`Renderer`], overriding its -/// [`Value`] if provided. -/// -/// [`Renderer`]: text::Renderer -pub fn draw<Theme, Renderer>( - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State<Renderer::Paragraph>, - value: &Value, - is_disabled: bool, - is_secure: bool, - icon: Option<&Icon<Renderer::Font>>, - style: &Theme::Style, - viewport: &Rectangle, -) where - Theme: StyleSheet, - Renderer: text::Renderer, -{ - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - let bounds = layout.bounds(); - - let mut children_layout = layout.children(); - let text_bounds = children_layout.next().unwrap().bounds(); - - let is_mouse_over = cursor.is_over(bounds); - - let appearance = if is_disabled { - theme.disabled(style) - } else 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: appearance.border, - ..renderer::Quad::default() - }, - appearance.background, - ); - - if icon.is_some() { - let icon_layout = children_layout.next().unwrap(); - - renderer.fill_paragraph( - &state.icon, - icon_layout.bounds().center(), - appearance.icon_color, - *viewport, - ); - } - - let text = value.to_string(); - - let (cursor, offset) = if let Some(focus) = state - .is_focused - .as_ref() - .filter(|focus| focus.is_window_focused) - { - match state.cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - position, - ); - - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; - - let cursor = if is_cursor_visible { - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - ..renderer::Quad::default() - }, - theme.value_color(style), - )) - } else { - None - }; - - (cursor, 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( - &state.value, - text_bounds, - left, - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - right, - ); - - 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, - }, - ..renderer::Quad::default() - }, - theme.selection_color(style), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let draw = |renderer: &mut Renderer, viewport| { - if let Some((cursor, color)) = cursor { - renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { - renderer.fill_quad(cursor, color); - }); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); - } - - renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, - Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), - if text.is_empty() { - theme.placeholder_color(style) - } else if is_disabled { - theme.disabled_color(style) - } else { - theme.value_color(style) - }, - viewport, - ); - }; - - if cursor.is_some() { - renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); - } else { - draw(renderer, text_bounds); - } -} - -/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - is_disabled: bool, -) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - if is_disabled { - mouse::Interaction::NotAllowed - } else { - mouse::Interaction::Text - } - } else { - mouse::Interaction::default() - } -} - -/// The state of a [`TextInput`]. -#[derive(Debug, Default, Clone)] -pub struct State<P: text::Paragraph> { - value: P, - placeholder: P, - icon: P, - is_focused: Option<Focus>, - is_dragging: bool, - is_pasting: Option<Value>, - last_click: Option<mouse::Click>, - cursor: Cursor, - keyboard_modifiers: keyboard::Modifiers, - // TODO: Add stateful horizontal scrolling offset -} +fn state<Renderer: text::Renderer>( + tree: &mut Tree, +) -> &mut State<Renderer::Paragraph> { + tree.state.downcast_mut::<State<Renderer::Paragraph>>() +} #[derive(Debug, Clone, Copy)] struct Focus { @@ -1463,3 +1384,95 @@ fn replace_paragraph<Renderer>( } const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + +/// The possible status of a [`TextInput`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`TextInput`] can be interacted with. + Active, + /// The [`TextInput`] is being hovered. + Hovered, + /// The [`TextInput`] is focused. + Focused, + /// The [`TextInput`] cannot be interacted with. + Disabled, +} + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The [`Border`] of the text input. + pub border: Border, + /// The [`Color`] of the icon of the text input. + pub icon: Color, + /// The [`Color`] of the placeholder of the text input. + pub placeholder: Color, + /// The [`Color`] of the value of the text input. + pub value: Color, + /// The [`Color`] of the selection of the text input. + pub selection: Color, +} + +/// The style of a [`TextInput`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`TextInput`]. +pub trait DefaultStyle { + /// Returns the default style of a [`TextInput`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// The default style of a [`TextInput`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + background: Background::Color(palette.background.base.color), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.background.strong.color, + }, + icon: palette.background.weak.text, + placeholder: palette.background.strong.color, + value: palette.background.base.text, + selection: palette.primary.weak.color, + }; + + match status { + Status::Active => active, + Status::Hovered => Appearance { + border: Border { + color: palette.background.base.text, + ..active.border + }, + ..active + }, + Status::Focused => Appearance { + border: Border { + color: palette.primary.strong.color, + ..active.border + }, + ..active + }, + Status::Disabled => Appearance { + background: Background::Color(palette.background.weak.color), + value: active.placeholder, + ..active + }, + } +} diff --git a/widget/src/themer.rs b/widget/src/themer.rs index e6ca6cfea2..a7eabd2c46 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,3 +1,4 @@ +use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -6,43 +7,67 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, - Widget, + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Size, Vector, Widget, }; +use std::marker::PhantomData; + /// A widget that applies any `Theme` to its contents. /// /// This widget can be useful to leverage multiple `Theme` /// types in an application. #[allow(missing_debug_implementations)] -pub struct Themer<'a, Message, Theme, Renderer> +pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { - content: Element<'a, Message, Theme, Renderer>, - theme: Theme, + content: Element<'a, Message, NewTheme, Renderer>, + to_theme: F, + text_color: Option<fn(&NewTheme) -> Color>, + background: Option<fn(&NewTheme) -> Background>, + old_theme: PhantomData<Theme>, } -impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> + Themer<'a, Message, Theme, NewTheme, F, Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { /// Creates an empty [`Themer`] that applies the given `Theme` /// to the provided `content`. - pub fn new<T>(theme: Theme, content: T) -> Self + pub fn new<T>(to_theme: F, content: T) -> Self where - T: Into<Element<'a, Message, Theme, Renderer>>, + T: Into<Element<'a, Message, NewTheme, Renderer>>, { Self { - theme, content: content.into(), + to_theme, + text_color: None, + background: None, + old_theme: PhantomData, } } + + /// Sets the default text [`Color`] of the [`Themer`]. + pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self { + self.text_color = Some(f); + self + } + + /// Sets the [`Background`] of the [`Themer`]. + pub fn background(mut self, f: fn(&NewTheme) -> Background) -> Self { + self.background = Some(f); + self + } } -impl<'a, AnyTheme, Message, Theme, Renderer> Widget<Message, AnyTheme, Renderer> - for Themer<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer> + for Themer<'a, Message, Theme, NewTheme, F, Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -119,21 +144,36 @@ where &self, tree: &Tree, renderer: &mut Renderer, - _theme: &AnyTheme, - renderer_style: &renderer::Style, + theme: &Theme, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.content.as_widget().draw( - tree, - renderer, - &self.theme, - renderer_style, - layout, - cursor, - viewport, - ); + let theme = (self.to_theme)(theme); + + if let Some(background) = self.background { + container::draw_background( + renderer, + &container::Appearance { + background: Some(background(&theme)), + ..container::Appearance::default() + }, + layout.bounds(), + ); + } + + let style = if let Some(text_color) = self.text_color { + renderer::Style { + text_color: text_color(&theme), + } + } else { + *style + }; + + self.content + .as_widget() + .draw(tree, renderer, &theme, &style, layout, cursor, viewport); } fn overlay<'b>( @@ -142,15 +182,15 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> { - struct Overlay<'a, Message, Theme, Renderer> { - theme: &'a Theme, - content: overlay::Element<'a, Message, Theme, Renderer>, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + struct Overlay<'a, Message, Theme, NewTheme, Renderer> { + to_theme: &'a dyn Fn(&Theme) -> NewTheme, + content: overlay::Element<'a, Message, NewTheme, Renderer>, } - impl<'a, AnyTheme, Message, Theme, Renderer> - overlay::Overlay<Message, AnyTheme, Renderer> - for Overlay<'a, Message, Theme, Renderer> + impl<'a, Message, Theme, NewTheme, Renderer> + overlay::Overlay<Message, Theme, Renderer> + for Overlay<'a, Message, Theme, NewTheme, Renderer> where Renderer: crate::core::Renderer, { @@ -165,13 +205,18 @@ where fn draw( &self, renderer: &mut Renderer, - _theme: &AnyTheme, + theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { - self.content - .draw(renderer, self.theme, style, layout, cursor); + self.content.draw( + renderer, + &(self.to_theme)(theme), + style, + layout, + cursor, + ); } fn on_event( @@ -220,12 +265,12 @@ where &'b mut self, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { self.content .overlay(layout, renderer) .map(|content| Overlay { - theme: self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) @@ -236,24 +281,26 @@ where .as_widget_mut() .overlay(tree, layout, renderer, translation) .map(|content| Overlay { - theme: &self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) } } -impl<'a, AnyTheme, Message, Theme, Renderer> - From<Themer<'a, Message, Theme, Renderer>> - for Element<'a, Message, AnyTheme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> + From<Themer<'a, Message, Theme, NewTheme, F, Renderer>> + for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: 'a, + NewTheme: 'a, + F: Fn(&Theme) -> NewTheme + 'a, Renderer: 'a + crate::core::Renderer, { fn from( - themer: Themer<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, AnyTheme, Renderer> { + themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { Element::new(themer) } } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 4e3925bad5..fc9e06e1d4 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -9,19 +9,16 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, - Shell, Size, Widget, + Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; -pub use crate::style::toggler::{Appearance, StyleSheet}; - /// A toggler widget. /// /// # Example /// /// ```no_run -/// # type Toggler<'a, Message> = -/// # iced_widget::Toggler<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>; /// # /// pub enum Message { /// TogglerToggled(bool), @@ -38,7 +35,6 @@ pub struct Toggler< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { is_toggled: bool, @@ -52,16 +48,15 @@ pub struct Toggler< text_shaping: text::Shaping, spacing: f32, font: Option<Renderer::Font>, - style: Theme::Style, + style: Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// The default size of a [`Toggler`]. - pub const DEFAULT_SIZE: f32 = 20.0; + pub const DEFAULT_SIZE: f32 = 16.0; /// Creates a new [`Toggler`]. /// @@ -77,6 +72,7 @@ where f: F, ) -> Self where + Theme: 'a + DefaultStyle, F: 'a + Fn(bool) -> Message, { Toggler { @@ -91,7 +87,7 @@ where text_shaping: text::Shaping::Basic, spacing: Self::DEFAULT_SIZE / 2.0, font: None, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -149,8 +145,11 @@ where } /// Sets the style of the [`Toggler`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } } @@ -158,7 +157,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -294,12 +292,18 @@ where let bounds = toggler_layout.bounds(); let is_mouse_over = cursor.is_over(layout.bounds()); - let style = if is_mouse_over { - theme.hovered(&self.style, self.is_toggled) + let status = if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } } else { - theme.active(&self.style, self.is_toggled) + Status::Active { + is_toggled: self.is_toggled, + } }; + let appearance = (self.style)(theme, status); + let border_radius = bounds.height / BORDER_RADIUS_RATIO; let space = SPACE_RATIO * bounds.height; @@ -315,12 +319,12 @@ where bounds: toggler_background_bounds, border: Border { radius: border_radius.into(), - width: style.background_border_width, - color: style.background_border_color, + width: appearance.background_border_width, + color: appearance.background_border_color, }, ..renderer::Quad::default() }, - style.background, + appearance.background, ); let toggler_foreground_bounds = Rectangle { @@ -340,12 +344,12 @@ where bounds: toggler_foreground_bounds, border: Border { radius: border_radius.into(), - width: style.foreground_border_width, - color: style.foreground_border_color, + width: appearance.foreground_border_width, + color: appearance.foreground_border_color, }, ..renderer::Quad::default() }, - style.foreground, + appearance.foreground, ); } } @@ -354,7 +358,7 @@ impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -363,3 +367,100 @@ where Element::new(toggler) } } + +/// The possible status of a [`Toggler`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Toggler`] can be interacted with. + Active { + /// Indicates whether the [`Toggler`] is toggled. + is_toggled: bool, + }, + /// The [`Toggler`] is being hovered. + Hovered { + /// Indicates whether the [`Toggler`] is toggled. + is_toggled: bool, + }, +} + +/// The appearance of a toggler. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The background [`Color`] of the toggler. + pub background: Color, + /// The width of the background border of the toggler. + pub background_border_width: f32, + /// The [`Color`] of the background border of the toggler. + pub background_border_color: Color, + /// The foreground [`Color`] of the toggler. + pub foreground: Color, + /// The width of the foreground border of the toggler. + pub foreground_border_width: f32, + /// The [`Color`] of the foreground border of the toggler. + pub foreground_border_color: Color, +} + +/// The style of a [`Toggler`]. +pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>; + +/// The default style of a [`Toggler`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Toggler`]. + fn default_style(&self, status: Status) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self, status: Status) -> Appearance { + default(self, status) + } +} + +impl DefaultStyle for Appearance { + fn default_style(&self, _status: Status) -> Appearance { + *self + } +} + +/// The default style of a [`Toggler`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let background = match status { + Status::Active { is_toggled } | Status::Hovered { is_toggled } => { + if is_toggled { + palette.primary.strong.color + } else { + palette.background.strong.color + } + } + }; + + let foreground = match status { + Status::Active { is_toggled } => { + if is_toggled { + palette.primary.strong.text + } else { + palette.background.base.color + } + } + Status::Hovered { is_toggled } => { + if is_toggled { + Color { + a: 0.5, + ..palette.primary.strong.text + } + } else { + palette.background.weak.color + } + } + }; + + Appearance { + background, + foreground, + foreground_border_width: 0.0, + foreground_border_color: Color::TRANSPARENT, + background_border_width: 0.0, + background_border_color: Color::TRANSPARENT, + } +} diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 3751739a73..32c962fc48 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -20,7 +20,6 @@ pub struct Tooltip< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -29,12 +28,11 @@ pub struct Tooltip< gap: f32, padding: f32, snap_within_viewport: bool, - style: <Theme as container::StyleSheet>::Style, + style: container::Style<'a, Theme>, } impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. @@ -47,7 +45,10 @@ where content: impl Into<Element<'a, Message, Theme, Renderer>>, tooltip: impl Into<Element<'a, Message, Theme, Renderer>>, position: Position, - ) -> Self { + ) -> Self + where + Theme: container::DefaultStyle + 'a, + { Tooltip { content: content.into(), tooltip: tooltip.into(), @@ -55,7 +56,7 @@ where gap: 0.0, padding: Self::DEFAULT_PADDING, snap_within_viewport: true, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -80,9 +81,9 @@ where /// Sets the style of the [`Tooltip`]. pub fn style( mut self, - style: impl Into<<Theme as container::StyleSheet>::Style>, + style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a, ) -> Self { - self.style = style.into(); + self.style = Box::new(style); self } } @@ -90,7 +91,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn children(&self) -> Vec<widget::Tree> { @@ -119,6 +119,10 @@ where self.content.as_widget().size() } + fn size_hint(&self) -> Size<Length> { + self.content.as_widget().size_hint() + } + fn layout( &self, tree: &mut widget::Tree, @@ -258,7 +262,7 @@ impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: container::StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -269,11 +273,10 @@ where } /// The position of the tooltip. Defaults to following the cursor. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Position { - /// The tooltip will follow the cursor. - FollowCursor, /// The tooltip will appear on the top of the widget. + #[default] Top, /// The tooltip will appear on the bottom of the widget. Bottom, @@ -281,6 +284,8 @@ pub enum Position { Left, /// The tooltip will appear on the right of the widget. Right, + /// The tooltip will follow the cursor. + FollowCursor, } #[derive(Debug, Clone, Copy, PartialEq, Default)] @@ -294,7 +299,6 @@ enum State { struct Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: container::StyleSheet + widget::text::StyleSheet, Renderer: text::Renderer, { position: Point, @@ -306,14 +310,14 @@ where positioning: Position, gap: f32, padding: f32, - style: &'b <Theme as container::StyleSheet>::Style, + style: + &'b (dyn Fn(&Theme, container::Status) -> container::Appearance + 'a), } impl<'a, 'b, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> for Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: container::StyleSheet + widget::text::StyleSheet, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -422,7 +426,7 @@ where layout: Layout<'_>, cursor_position: mouse::Cursor, ) { - let style = container::StyleSheet::appearance(theme, self.style); + let style = (self.style)(theme, container::Status::Idle); container::draw_background(renderer, &style, layout.bounds()); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 8f7c88da72..2aa8f4d1f0 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -1,9 +1,9 @@ //! Display an interactive selector of a single value from a range of values. -//! -//! A [`VerticalSlider`] has some local [`State`]. use std::ops::RangeInclusive; -pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; +pub use crate::slider::{ + default, Appearance, DefaultStyle, Handle, HandleShape, Status, Style, +}; use crate::core; use crate::core::event::{self, Event}; @@ -29,8 +29,7 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type VerticalSlider<'a, T, Message> = -/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::style::Theme>; +/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>; /// # /// #[derive(Clone)] /// pub enum Message { @@ -42,10 +41,7 @@ use crate::core::{ /// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); /// ``` #[allow(missing_debug_implementations)] -pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> { range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -55,17 +51,16 @@ where on_release: Option<Message>, width: f32, height: Length, - style: Theme::Style, + style: Style<'a, Theme>, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: StyleSheet, { /// The default width of a [`VerticalSlider`]. - pub const DEFAULT_WIDTH: f32 = 22.0; + pub const DEFAULT_WIDTH: f32 = 16.0; /// Creates a new [`VerticalSlider`]. /// @@ -77,6 +72,7 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where + Theme: DefaultStyle + 'a, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -101,7 +97,7 @@ where on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, - style: Default::default(), + style: Box::new(Theme::default_style), } } @@ -137,8 +133,11 @@ where } /// Sets the style of the [`VerticalSlider`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style( + mut self, + style: impl Fn(&Theme, Status) -> Appearance + 'a, + ) -> Self { + self.style = Box::new(style); self } @@ -162,7 +161,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Theme: StyleSheet, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { @@ -170,7 +168,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -200,360 +198,287 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::<State>(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) - } + let state = tree.state.downcast_mut::<State>(); + let is_dragging = state.is_dragging; + let current_value = self.value; - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, - theme, - &self.style, - ); - } + let locate = |cursor_position: Point| -> Option<T> { + let bounds = layout.bounds(); - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) - } -} + let new_value = if cursor_position.y >= bounds.y + bounds.height { + Some(*self.range.start()) + } else if cursor_position.y <= bounds.y { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); -impl<'a, T, Message, Theme, Renderer> - From<VerticalSlider<'a, T, Message, Theme>> - for Element<'a, Message, Theme, Renderer> -where - T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, - Message: Clone + 'a, - Theme: StyleSheet + 'a, - Renderer: core::Renderer + 'a, -{ - fn from( - slider: VerticalSlider<'a, T, Message, Theme>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(slider) - } -} + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); -/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`] -/// accordingly. -pub fn update<Message, T>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - default: Option<T>, - range: &RangeInclusive<T>, - step: T, - shift_step: Option<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 current_value = *value; + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); - let locate = |cursor_position: Point| -> Option<T> { - let bounds = layout.bounds(); + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; - let new_value = if cursor_position.y >= bounds.y + bounds.height { - Some(*range.start()) - } else if cursor_position.y <= bounds.y { - Some(*range.end()) - } else { + T::from_f64(value) + }; + + new_value + }; + + let increment = |value: T| -> Option<T> { let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) + self.shift_step.unwrap_or(self.step) } else { - step + self.step } .into(); - let start = (*range.start()).into(); - let end = (*range.end()).into(); - - let percent = 1.0 - - f64::from(cursor_position.y - bounds.y) - / f64::from(bounds.height); + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } - T::from_f64(value) + T::from_f64(new_value) }; - new_value - }; - - let increment = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); - - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); - - if new_value > (*range.end()).into() { - return Some(*range.end()); - } - - T::from_f64(new_value) - }; + let decrement = |value: T| -> Option<T> { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); - let decrement = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) - } else { - step - } - .into(); + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } - if new_value < (*range.start()).into() { - return Some(*range.start()); - } + T::from_f64(new_value) + }; - T::from_f64(new_value) - }; + let change = |new_value: T| { + if (self.value.into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((self.on_change)(new_value)); - let change = |new_value: T| { - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); + self.value = new_value; + } + }; - *value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.control() - || state.keyboard_modifiers.command() + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_over(layout.bounds()) { - let _ = default.map(change); - state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; - } + if state.keyboard_modifiers.control() + || state.keyboard_modifiers.command() + { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(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); + return event::Status::Captured; } - state.is_dragging = false; + } + 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); + } + state.is_dragging = false; - return event::Status::Captured; + return event::Status::Captured; + } } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + let _ = cursor.position().and_then(locate).map(change); - return event::Status::Captured; + return event::Status::Captured; + } } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { - match key { - Key::Named(key::Named::ArrowUp) => { - let _ = increment(current_value).map(change); + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if cursor.position_over(layout.bounds()).is_some() { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } - return event::Status::Captured; + return event::Status::Captured; + } } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } + _ => {} } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} + + event::Status::Ignored } - event::Status::Ignored -} + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); -/// Draws a [`VerticalSlider`]. -pub fn draw<T, Theme, Renderer>( - renderer: &mut Renderer, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, - value: T, - range: &RangeInclusive<T>, - style_sheet: &Theme, - style: &Theme::Style, -) where - T: Into<f64> + Copy, - Theme: StyleSheet, - Renderer: core::Renderer, -{ - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - 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 (handle_width, handle_height, handle_border_radius) = - match style.handle.shape { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), bounds.width, border_radius), + let style = (self.style)( + theme, + if state.is_dragging { + Status::Dragged + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }, + ); + + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (radius * 2.0, radius * 2.0, radius.into()) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), bounds.width, 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 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 offset = if range_start >= range_end { - 0.0 - } else { - (bounds.height - handle_width) * (value - range_end) - / (range_start - range_end) - }; - - let rail_x = bounds.x + bounds.width / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y, - width: style.rail.width, - height: offset + handle_width / 2.0, - }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.1, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - style.rail.width / 2.0, - y: bounds.y + offset + handle_width / 2.0, - width: style.rail.width, - height: bounds.height - offset - handle_width / 2.0, + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.height - handle_width) * (value - range_end) + / (range_start - range_end) + }; + + let rail_x = bounds.x + bounds.width / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border: Border::rounded(style.rail.border_radius), + ..renderer::Quad::default() }, - border: Border::with_radius(style.rail.border_radius), - ..renderer::Quad::default() - }, - style.rail.colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - handle_height / 2.0, - y: bounds.y + offset, - width: handle_height, - height: handle_width, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset - handle_width / 2.0, + }, + border: Border::rounded(style.rail.border_radius), + ..renderer::Quad::default() }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - handle_height / 2.0, + y: bounds.y + offset, + width: handle_height, + height: handle_width, + }, + border: Border { + radius: handle_border_radius, + width: style.handle.border_width, + color: style.handle.border_color, + }, + ..renderer::Quad::default() }, - ..renderer::Quad::default() - }, - style.handle.color, - ); + style.handle.color, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } + } } -/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - - if state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() +impl<'a, T, Message, Theme, Renderer> + From<VerticalSlider<'a, T, Message, Theme>> + for Element<'a, Message, Theme, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, + Message: Clone + 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + slider: VerticalSlider<'a, T, Message, Theme>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(slider) } } -/// The local state of a [`VerticalSlider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { +struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, } - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 87e600aeba..9d65cc1baa 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -24,7 +24,6 @@ multi-window = ["iced_runtime/multi-window"] [dependencies] iced_graphics.workspace = true iced_runtime.workspace = true -iced_style.workspace = true log.workspace = true thiserror.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 77e2c83e7c..13d9282d46 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -10,7 +10,7 @@ use crate::core::renderer; use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; -use crate::core::{Event, Size}; +use crate::core::{Color, Event, Point, Size, Theme}; use crate::futures::futures; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::compositor::{self, Compositor}; @@ -18,7 +18,6 @@ use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Command, Debug}; -use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; @@ -39,7 +38,7 @@ use std::sync::Arc; /// can be toggled by pressing `F12`. pub trait Application: Program where - Self::Theme: StyleSheet, + Self::Theme: DefaultStyle, { /// The data needed to initialize your [`Application`]. type Flags; @@ -64,8 +63,8 @@ where fn theme(&self) -> Self::Theme; /// Returns the `Style` variation of the `Theme`. - fn style(&self) -> <Self::Theme as StyleSheet>::Style { - Default::default() + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// Returns the event `Subscription` for the current state of the @@ -95,9 +94,41 @@ where } } +/// The appearance of an application. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The background [`Color`] of the application. + pub background_color: Color, + + /// The default text [`Color`] of the application. + pub text_color: Color, +} + +/// The default style of an [`Application`]. +pub trait DefaultStyle { + /// Returns the default style of an [`Application`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +/// The default [`Appearance`] of an [`Application`] with the built-in [`Theme`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background_color: palette.background.base.color, + text_color: palette.background.base.text, + } +} + /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. -pub fn run<A, E, C>( +pub async fn run<A, E, C>( settings: Settings<A::Flags>, compositor_settings: C::Settings, ) -> Result<(), Error> @@ -105,7 +136,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use futures::task; use futures::Future; @@ -159,6 +190,10 @@ where use winit::platform::web::WindowExtWebSys; let canvas = window.canvas().expect("Get window canvas"); + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -184,7 +219,7 @@ where }; } - let compositor = C::new(compositor_settings, window.clone())?; + let compositor = C::new(compositor_settings, window.clone()).await?; let mut renderer = compositor.create_renderer(); for font in settings.fonts { @@ -250,7 +285,8 @@ where if matches!( event, winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(_), + event: winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_), .. } ) { @@ -284,7 +320,7 @@ async fn run_instance<A, E, C>( A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use futures::stream::StreamExt; use winit::event; @@ -607,7 +643,7 @@ pub fn build_user_interface<'a, A: Application>( debug: &mut Debug, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { debug.view_started(); let view = application.view(); @@ -638,7 +674,7 @@ pub fn update<A: Application, C, E: Executor>( window: &winit::window::Window, ) where C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); @@ -689,7 +725,7 @@ pub fn run_command<A, C, E>( A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::command; use crate::runtime::system; @@ -704,15 +740,15 @@ pub fn run_command<A, C, E>( runtime.run(stream); } command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag) => { - let message = tag(clipboard.read()); + clipboard::Action::Read(tag, kind) => { + let message = tag(clipboard.read(kind)); proxy .send_event(message) .expect("Send message to event loop"); } - clipboard::Action::Write(contents) => { - clipboard.write(contents); + clipboard::Action::Write(contents, kind) => { + clipboard.write(kind, contents); } }, command::Action::Window(action) => match action { @@ -762,6 +798,21 @@ pub fn run_command<A, C, E>( window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); } + window::Action::FetchPosition(_id, callback) => { + let position = window + .inner_position() + .map(|position| { + let position = position + .to_logical::<f32>(window.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + proxy + .send_event(callback(position)) + .expect("Send message to event loop"); + } window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { x: position.x, @@ -806,6 +857,14 @@ pub fn run_command<A, C, E>( window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } + window::Action::ShowSystemMenu(_id) => { + if let mouse::Cursor::Available(point) = state.cursor() { + window.show_window_menu(winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }); + } + } window::Action::FetchId(_id, tag) => { proxy .send_event(tag(window.id().into())) diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c17a3bcc13..a0a0693310 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -1,4 +1,4 @@ -use crate::application::{self, StyleSheet as _}; +use crate::application; use crate::conversion; use crate::core::mouse; use crate::core::{Color, Size}; @@ -14,7 +14,7 @@ use winit::window::Window; #[allow(missing_debug_implementations)] pub struct State<A: Application> where - A::Theme: application::StyleSheet, + A::Theme: application::DefaultStyle, { title: String, scale_factor: f64, @@ -29,14 +29,14 @@ where impl<A: Application> State<A> where - A::Theme: application::StyleSheet, + A::Theme: application::DefaultStyle, { /// Creates a new [`State`] for the provided [`Application`] and window. pub fn new(application: &A, window: &Window) -> Self { let title = application.title(); let scale_factor = application.scale_factor(); let theme = application.theme(); - let appearance = theme.appearance(&application.style()); + let appearance = application.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -216,6 +216,6 @@ where // Update theme and appearance self.theme = application.theme(); - self.appearance = self.theme.appearance(&application.style()); + self.appearance = application.style(&self.theme); } } diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 8f5c5e63e7..5237ca0152 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,5 +1,7 @@ //! Access the clipboard. +use crate::core::clipboard::Kind; + /// A buffer for short-term storage and transfer within and between /// applications. #[allow(missing_debug_implementations)] @@ -33,33 +35,45 @@ impl Clipboard { } /// Reads the current content of the [`Clipboard`] as text. - pub fn read(&self) -> Option<String> { + pub fn read(&self, kind: Kind) -> Option<String> { match &self.state { - State::Connected(clipboard) => clipboard.read().ok(), + State::Connected(clipboard) => match kind { + Kind::Standard => clipboard.read().ok(), + Kind::Primary => clipboard.read_primary().and_then(Result::ok), + }, State::Unavailable => None, } } /// Writes the given text contents to the [`Clipboard`]. - pub fn write(&mut self, contents: String) { + pub fn write(&mut self, kind: Kind, contents: String) { match &mut self.state { - State::Connected(clipboard) => match clipboard.write(contents) { - Ok(()) => {} - Err(error) => { - log::warn!("error writing to clipboard: {error}"); + State::Connected(clipboard) => { + let result = match kind { + Kind::Standard => clipboard.write(contents), + Kind::Primary => { + clipboard.write_primary(contents).unwrap_or(Ok(())) + } + }; + + match result { + Ok(()) => {} + Err(error) => { + log::warn!("error writing to clipboard: {error}"); + } } - }, + } State::Unavailable => {} } } } impl crate::core::Clipboard for Clipboard { - fn read(&self) -> Option<String> { - self.read() + fn read(&self, kind: Kind) -> Option<String> { + self.read(kind) } - fn write(&mut self, contents: String) { - self.write(contents); + fn write(&mut self, kind: Kind, contents: String) { + self.write(kind, contents); } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index ef789296e2..fc3d1c086b 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -1,7 +1,7 @@ //! Convert [`winit`] types into [`iced_runtime`] types, and viceversa. //! //! [`winit`]: https://github.com/rust-windowing/winit -//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.10/runtime +//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.12/runtime use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -195,17 +195,40 @@ pub fn window_event( })) } }, - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key, - state, - text, - location, - .. - }, - .. - } => Some(Event::Keyboard({ + WindowEvent::KeyboardInput { event, .. } => Some(Event::Keyboard({ + let logical_key = { + #[cfg(not(target_arch = "wasm32"))] + { + use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + event.key_without_modifiers() + } + + #[cfg(target_arch = "wasm32")] + { + // TODO: Fix inconsistent API on Wasm + event.logical_key + } + }; + + let text = { + #[cfg(not(target_arch = "wasm32"))] + { + use crate::core::SmolStr; + use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + + event.text_with_all_modifiers().map(SmolStr::new) + } + + #[cfg(target_arch = "wasm32")] + { + // TODO: Fix inconsistent API on Wasm + event.text + } + }.filter(|text| !text.as_str().chars().any(is_private_use)); + + let winit::event::KeyEvent { + state, location, .. + } = event; let key = key(logical_key); let modifiers = self::modifiers(modifiers); @@ -392,7 +415,7 @@ pub fn mouse_interaction( /// Converts a `MouseButton` from [`winit`] to an [`iced`] mouse button. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { match mouse_button { winit::event::MouseButton::Left => mouse::Button::Left, @@ -408,7 +431,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// state. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 pub fn modifiers( modifiers: winit::keyboard::ModifiersState, ) -> keyboard::Modifiers { @@ -435,7 +458,7 @@ pub fn cursor_position( /// Converts a `Touch` from [`winit`] to an [`iced`] touch event. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 pub fn touch_event( touch: winit::event::Touch, scale_factor: f64, @@ -466,7 +489,7 @@ pub fn touch_event( /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 pub fn key(key: winit::keyboard::Key) -> keyboard::Key { use keyboard::key::Named; use winit::keyboard::NamedKey; @@ -816,3 +839,8 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } + +// See: https://en.wikipedia.org/wiki/Private_Use_Areas +fn is_private_use(c: char) -> bool { + ('\u{E000}'..='\u{F8FF}').contains(&c) +} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 948576a28a..64912b3f34 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -11,7 +11,7 @@ //! Additionally, a [`conversion`] module is available for users that decide to //! implement a custom event loop. //! -//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.10/runtime +//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.12/runtime //! [`winit`]: https://github.com/rust-windowing/winit //! [`conversion`]: crate::conversion #![doc( @@ -30,7 +30,6 @@ pub use iced_graphics as graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; -pub use iced_style as style; pub use winit; #[cfg(feature = "multi-window")] diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 23b2f3c4cd..18db1fb514 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -6,12 +6,15 @@ pub use state::State; use crate::conversion; use crate::core; +use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation; use crate::core::window; -use crate::core::Size; +use crate::core::{Point, Size}; use crate::futures::futures::channel::mpsc; -use crate::futures::futures::{task, Future, StreamExt}; +use crate::futures::futures::executor; +use crate::futures::futures::task; +use crate::futures::futures::{Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; @@ -19,9 +22,10 @@ use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; -use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +pub use crate::application::{default, Appearance, DefaultStyle}; + use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -40,7 +44,7 @@ use std::time::Instant; /// can be toggled by pressing `F12`. pub trait Application: Program where - Self::Theme: StyleSheet, + Self::Theme: DefaultStyle, { /// The data needed to initialize your [`Application`]. type Flags; @@ -65,8 +69,8 @@ where fn theme(&self, window: window::Id) -> Self::Theme; /// Returns the `Style` variation of the `Theme`. - fn style(&self) -> <Self::Theme as StyleSheet>::Style { - Default::default() + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// Returns the event `Subscription` for the current state of the @@ -107,7 +111,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use winit::event_loop::EventLoopBuilder; @@ -182,7 +186,8 @@ where }; } - let mut compositor = C::new(compositor_settings, main_window.clone())?; + let mut compositor = + executor::block_on(C::new(compositor_settings, main_window.clone()))?; let mut window_manager = WindowManager::new(); let _ = window_manager.insert( @@ -211,7 +216,7 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); - let _ = event_loop.run(move |event, event_loop| { + let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { if event_loop.exiting() { return; } @@ -280,7 +285,35 @@ where } }; } - }); + }; + + #[cfg(not(target_os = "windows"))] + let _ = event_loop.run(process_event); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + let mut process_event = process_event; + + let _ = event_loop.run(move |event, event_loop| { + if matches!( + event, + winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_), + .. + } + ) { + process_event(event, event_loop); + process_event(winit::event::Event::AboutToWait, event_loop); + } else { + process_event(event, event_loop); + } + }); + } Ok(()) } @@ -320,7 +353,7 @@ async fn run_instance<A, E, C>( A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use winit::event; use winit::event_loop::ControlFlow; @@ -790,7 +823,7 @@ fn build_user_interface<'a, A: Application>( id: window::Id, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { debug.view_started(); let view = application.view(id); @@ -818,7 +851,7 @@ fn update<A: Application, C, E: Executor>( ui_caches: &mut HashMap<window::Id, user_interface::Cache>, ) where C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); @@ -861,7 +894,7 @@ fn run_command<A, C, E>( A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::clipboard; use crate::runtime::system; @@ -876,15 +909,15 @@ fn run_command<A, C, E>( runtime.run(Box::pin(stream)); } command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag) => { - let message = tag(clipboard.read()); + clipboard::Action::Read(tag, kind) => { + let message = tag(clipboard.read(kind)); proxy .send_event(message) .expect("Send message to event loop"); } - clipboard::Action::Write(contents) => { - clipboard.write(contents); + clipboard::Action::Write(contents, kind) => { + clipboard.write(kind, contents); } }, command::Action::Window(action) => match action { @@ -964,6 +997,25 @@ fn run_command<A, C, E>( window.raw.set_minimized(minimized); } } + window::Action::FetchPosition(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + let position = window + .raw + .inner_position() + .map(|position| { + let position = position.to_logical::<f32>( + window.raw.scale_factor(), + ); + + Point::new(position.x, position.y) + }) + .ok(); + + proxy + .send_event(callback(position)) + .expect("Send message to event loop"); + } + } window::Action::Move(id, position) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_outer_position( @@ -1030,6 +1082,20 @@ fn run_command<A, C, E>( .set_window_level(conversion::window_level(level)); } } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { proxy @@ -1154,8 +1220,8 @@ pub fn build_user_interfaces<'a, A: Application, C: Compositor>( mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, ) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> where - A::Theme: StyleSheet, C: Compositor<Renderer = A::Renderer>, + A::Theme: DefaultStyle, { cached_user_interfaces .drain() diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2e97a13d96..dfd8e69683 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -2,18 +2,16 @@ use crate::conversion; use crate::core::{mouse, window}; use crate::core::{Color, Size}; use crate::graphics::Viewport; -use crate::multi_window::Application; -use crate::style::application; +use crate::multi_window::{self, Application}; use std::fmt::{Debug, Formatter}; -use iced_style::application::StyleSheet; use winit::event::{Touch, WindowEvent}; use winit::window::Window; /// The state of a multi-windowed [`Application`]. pub struct State<A: Application> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { title: String, scale_factor: f64, @@ -22,12 +20,12 @@ where cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::keyboard::ModifiersState, theme: A::Theme, - appearance: application::Appearance, + appearance: multi_window::Appearance, } impl<A: Application> Debug for State<A> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("multi_window::State") @@ -43,7 +41,7 @@ where impl<A: Application> State<A> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { /// Creates a new [`State`] for the provided [`Application`]'s `window`. pub fn new( @@ -54,7 +52,7 @@ where let title = application.title(window_id); let scale_factor = application.scale_factor(window_id); let theme = application.theme(window_id); - let appearance = theme.appearance(&application.style()); + let appearance = application.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -236,6 +234,6 @@ where // Update theme and appearance self.theme = application.theme(window_id); - self.appearance = self.theme.appearance(&application.style()); + self.appearance = application.style(&self.theme); } } diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 23f3c0ba0b..71c1688b80 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -2,8 +2,7 @@ use crate::core::mouse; use crate::core::window::Id; use crate::core::{Point, Size}; use crate::graphics::Compositor; -use crate::multi_window::{Application, State}; -use crate::style::application::StyleSheet; +use crate::multi_window::{Application, DefaultStyle, State}; use std::collections::BTreeMap; use std::sync::Arc; @@ -12,8 +11,8 @@ use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] pub struct WindowManager<A: Application, C: Compositor> where - A::Theme: StyleSheet, C: Compositor<Renderer = A::Renderer>, + A::Theme: DefaultStyle, { aliases: BTreeMap<winit::window::WindowId, Id>, entries: BTreeMap<Id, Window<A, C>>, @@ -23,7 +22,7 @@ impl<A, C> WindowManager<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub fn new() -> Self { Self { @@ -109,7 +108,7 @@ impl<A, C> Default for WindowManager<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { fn default() -> Self { Self::new() @@ -121,7 +120,7 @@ pub struct Window<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub raw: Arc<winit::window::Window>, pub state: State<A>, @@ -136,7 +135,7 @@ impl<A, C> Window<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub fn position(&self) -> Option<Point> { self.raw