diff --git a/.cargo/config.toml b/.cargo/config.toml index 85a46cda..49ca3252 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,53 +1,2 @@ [alias] -lint = """ -clippy --workspace --no-deps -- \ - -D warnings \ - -A clippy::type_complexity \ - -D clippy::semicolon_if_nothing_returned \ - -D clippy::trivially-copy-pass-by-ref \ - -D clippy::default_trait_access \ - -D clippy::match-wildcard-for-single-variants \ - -D clippy::redundant-closure-for-method-calls \ - -D clippy::filter_map_next \ - -D clippy::manual_let_else \ - -D clippy::unused_async \ - -D clippy::from_over_into \ - -D clippy::needless_borrow \ - -D clippy::new_without_default \ - -D clippy::useless_conversion -""" - -nitpick = """ -clippy --workspace --no-deps -- \ - -D warnings \ - -D clippy::pedantic \ - -A clippy::type_complexity \ - -A clippy::must_use_candidate \ - -A clippy::return_self_not_must_use \ - -A clippy::needless_pass_by_value \ - -A clippy::cast_precision_loss \ - -A clippy::cast_sign_loss \ - -A clippy::cast_possible_truncation \ - -A clippy::match_same_arms \ - -A clippy::missing-errors-doc \ - -A clippy::missing-panics-doc \ - -A clippy::cast_lossless \ - -A clippy::doc_markdown \ - -A clippy::items_after_statements \ - -A clippy::too_many_lines \ - -A clippy::module_name_repetitions \ - -A clippy::if_not_else \ - -A clippy::redundant_else \ - -A clippy::used_underscore_binding \ - -A clippy::cast_possible_wrap \ - -A clippy::unnecessary_wraps \ - -A clippy::struct-excessive-bools \ - -A clippy::float-cmp \ - -A clippy::single_match_else \ - -A clippy::unreadable_literal \ - -A clippy::explicit_deref_methods \ - -A clippy::map_unwrap_or \ - -A clippy::unnested_or_patterns \ - -A clippy::similar_names \ - -A clippy::unused_self -""" +lint = "clippy --workspace --benches --all-features --no-deps -- -D warnings" diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 09b31697..20ef2b73 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 d5c56ac0..40e9235a 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 @@ -20,7 +20,7 @@ jobs: # artifacts: # runs-on: ubuntu-latest # steps: - # - uses: hecrj/setup-rust-action@v1 + # - uses: hecrj/setup-rust-action@v2 # - name: Install cargo-outdated # run: cargo install cargo-outdated # - uses: actions/checkout@master diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7cfbff89..ba1ab003 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 df9c480f..8f5e65d6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,20 +1,12 @@ name: Check on: [push, pull_request] jobs: - widget: - runs-on: ubuntu-latest - steps: - - uses: hecrj/setup-rust-action@v1 - - uses: actions/checkout@master - - name: Check standalone `iced_widget` crate - run: cargo check --package iced_widget --features image,svg,canvas - wasm: runs-on: ubuntu-latest env: RUSTFLAGS: --cfg=web_sys_unstable_apis steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: rust-version: stable targets: wasm32-unknown-unknown @@ -25,5 +17,11 @@ jobs: run: cargo build --package tour --target wasm32-unknown-unknown - name: Check compilation of `todos` example run: cargo build --package todos --target wasm32-unknown-unknown - - name: Check compilation of `integration` example - run: cargo build --package integration --target wasm32-unknown-unknown + + widget: + runs-on: ubuntu-latest + steps: + - 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 diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 35bf10f4..827a2ca8 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 \ @@ -28,6 +27,8 @@ jobs: -p iced - name: Write CNAME file run: echo 'docs.iced.rs' > ./target/doc/CNAME + - name: Copy redirect file as index.html + run: cp docs/redirect.html target/doc/index.html - name: Publish documentation if: github.ref == 'refs/heads/master' uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 42a96411..3aa7eaf3 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 2ff86614..16ee8bf9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,11 +2,16 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: macOS-latest + runs-on: ubuntu-latest steps: - - uses: hecrj/setup-rust-action@v1 + - uses: hecrj/setup-rust-action@v2 with: components: clippy - uses: actions/checkout@master + - name: Install dependencies + run: | + export DEBIAN_FRONTED=noninteractive + sudo apt-get -qq update + sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Check lints run: cargo lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c5ee0d9..47c61f5e 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/Cargo.toml b/Cargo.toml index 336133b2..2ea64300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -18,9 +21,11 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "fira-sans"] +default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] +# Enable the `tiny-skia` software renderer backend +tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `Image` widget image = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget @@ -39,8 +44,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 @@ -52,9 +55,11 @@ highlighter = ["iced_highlighter"] # Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] # Enables the advanced module -advanced = [] +advanced = ["iced_core/advanced", "iced_widget/advanced"] # Enables embedding Fira Sans as the default font on Wasm builds fira-sans = ["iced_renderer/fira-sans"] +# Enables auto-detecting light/dark mode for the built-in theme +auto-detect-theme = ["iced_core/auto-detect-theme"] [dependencies] iced_core.workspace = true @@ -73,6 +78,15 @@ thiserror.workspace = true image.workspace = true image.optional = true +[dev-dependencies] +criterion = "0.5" +iced_wgpu.workspace = true + +[[bench]] +name = "wgpu" +harness = false +required-features = ["canvas"] + [profile.release-opt] inherits = "release" codegen-units = 1 @@ -93,7 +107,6 @@ members = [ "renderer", "runtime", "sentinel", - "style", "tiny_skia", "wgpu", "widget", @@ -121,7 +134,6 @@ 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_sentinel = { version = "0.13.0-dev", path = "sentinel" } -iced_style = { version = "0.13.0-dev", path = "style" } 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" } @@ -131,10 +143,12 @@ async-std = "1.0" bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } +bytes = "1.6" cosmic-text = "0.10" +dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = "0.5" +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -162,13 +176,37 @@ thiserror = "1.0" tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" -xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "=0.3.67" -web-time = "0.2" +web-time = "1.1" wgpu = "0.19" winapi = "0.3" window_clipboard = "0.4.1" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" } + +[workspace.lints.rust] +rust_2018_idioms = "deny" +missing_debug_implementations = "deny" +missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[workspace.lints.clippy] +type-complexity = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +default_trait_access = "deny" +match-wildcard-for-single-variants = "deny" +redundant-closure-for-method-calls = "deny" +filter_map_next = "deny" +manual_let_else = "deny" +unused_async = "deny" +from_over_into = "deny" +needless_borrow = "deny" +new_without_default = "deny" +useless_conversion = "deny" + +[workspace.lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 809371cb..5d738d85 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -20,7 +20,7 @@ pkgs.mkShell rec { freetype freetype.dev libGL - pkgconfig + pkg-config xorg.libX11 xorg.libXcursor xorg.libXi diff --git a/README.md b/README.md index 9f21fc83..0db09ded 100644 --- a/README.md +++ b/README.md @@ -98,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, } ``` @@ -110,8 +110,8 @@ the button presses. These interactions are our __messages__: ```rust #[derive(Debug, Clone, Copy)] pub enum Message { - IncrementPressed, - DecrementPressed, + Increment, + Decrement, } ``` @@ -126,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), ] } } @@ -149,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; } } @@ -160,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 @@ -208,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/benches/ipsum.txt b/benches/ipsum.txt new file mode 100644 index 00000000..3e2d6396 --- /dev/null +++ b/benches/ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at elit mollis, dictum nunc non, tempus metus. Sed iaculis ac mauris eu lobortis. Integer elementum venenatis eros, id placerat odio feugiat vel. Maecenas consequat convallis tincidunt. Nunc eu lorem justo. Praesent quis ornare sapien. Aliquam interdum tortor ut rhoncus faucibus. Suspendisse molestie scelerisque nulla, eget sodales lacus sodales vel. Nunc placerat id arcu sodales venenatis. Praesent ullamcorper viverra nibh eget efficitur. Aliquam molestie felis vehicula, finibus sapien eget, accumsan purus. Praesent vestibulum eleifend consectetur. Sed tincidunt lectus a libero efficitur, non scelerisque lectus tincidunt. + +Cras ullamcorper tincidunt tellus non tempor. Integer pulvinar turpis quam, nec pharetra purus egestas non. Vivamus sed ipsum consequat, dignissim ante et, suscipit nibh. Quisque et mauris eu erat rutrum cursus. Pellentesque ut neque eu neque eleifend auctor ac hendrerit dolor. Morbi eget egestas ex. Integer hendrerit ipsum in enim bibendum, at vehicula ipsum dapibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempus consectetur tortor, vel fermentum sem pulvinar eget. Maecenas rutrum fringilla eros a pellentesque. Cras quis magna consectetur, tristique massa vel, aliquet nunc. Aliquam erat volutpat. Suspendisse porttitor risus id auctor fermentum. Vivamus efficitur tellus sed tortor cursus tincidunt. Sed auctor varius arcu, non congue tellus vehicula finibus. + +Fusce a tincidunt urna. Nunc at quam ac enim tempor vehicula imperdiet in sapien. Donec lobortis tristique felis vel semper. Quisque vulputate felis eu enim vestibulum malesuada. Fusce a lobortis mauris, iaculis eleifend ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus sodales vel elit dignissim mattis. + +Aliquam placerat vulputate dignissim. Proin pellentesque vitae arcu ut feugiat. Nunc mi felis, ornare at gravida sed, vestibulum sed urna. Duis fermentum maximus viverra. Donec imperdiet pellentesque sollicitudin. Cras non sem quis metus bibendum molestie. Duis imperdiet nec lectus eu rutrum. Mauris congue enim purus, in iaculis arcu dapibus ut. Nullam id erat tincidunt, iaculis dolor non, lobortis magna. Proin convallis scelerisque maximus. Morbi at lorem fringilla libero blandit fringilla. Ut aliquet tellus non sem dictum viverra. Aenean venenatis purus eget lacus placerat, non mollis mauris pellentesque. + +Etiam elit diam, aliquet quis suscipit non, condimentum viverra odio. Praesent mi enim, suscipit id mi in, rhoncus ultricies lorem. Nulla facilisi. Integer convallis sagittis euismod. Vestibulum porttitor sodales turpis ac accumsan. Nullam molestie turpis vel lacus tincidunt, sed finibus erat pharetra. Nullam vestibulum turpis id sollicitudin accumsan. Praesent eget posuere lacus. Donec vehicula, nisl nec suscipit porta, felis lorem gravida orci, a hendrerit tellus nibh sit amet elit. diff --git a/benches/wgpu.rs b/benches/wgpu.rs new file mode 100644 index 00000000..cc90bb38 --- /dev/null +++ b/benches/wgpu.rs @@ -0,0 +1,227 @@ +#![allow(missing_docs)] +use criterion::{criterion_group, criterion_main, Bencher, Criterion}; + +use iced::alignment; +use iced::mouse; +use iced::widget::{canvas, scrollable, stack, text}; +use iced::{ + Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, +}; +use iced_wgpu::Renderer; + +criterion_main!(benches); +criterion_group!(benches, wgpu_benchmark); + +#[allow(unused_results)] +pub fn wgpu_benchmark(c: &mut Criterion) { + c.bench_function("wgpu — canvas (light)", |b| { + benchmark(b, |_| scene(10)); + }); + c.bench_function("wgpu — canvas (heavy)", |b| { + benchmark(b, |_| scene(1_000)); + }); + + c.bench_function("wgpu - layered text (light)", |b| { + benchmark(b, |_| layered_text(10)); + }); + c.bench_function("wgpu - layered text (heavy)", |b| { + benchmark(b, |_| layered_text(1_000)); + }); + + c.bench_function("wgpu - dynamic text (light)", |b| { + benchmark(b, |i| dynamic_text(1_000, i)); + }); + c.bench_function("wgpu - dynamic text (heavy)", |b| { + benchmark(b, |i| dynamic_text(100_000, i)); + }); +} + +fn benchmark<'a>( + bencher: &mut Bencher<'_>, + view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, +) { + use iced_futures::futures::executor; + use iced_wgpu::graphics; + use iced_wgpu::graphics::Antialiasing; + use iced_wgpu::wgpu; + use iced_winit::core; + use iced_winit::runtime; + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + + let adapter = executor::block_on(instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }, + )) + .expect("request adapter"); + + let (device, queue) = executor::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + }, + None, + )) + .expect("request device"); + + let format = wgpu::TextureFormat::Bgra8UnormSrgb; + + let mut engine = iced_wgpu::Engine::new( + &adapter, + &device, + &queue, + format, + Some(Antialiasing::MSAAx4), + ); + + let mut renderer = + Renderer::new(&device, &engine, Font::DEFAULT, Pixels::from(16)); + + let viewport = + graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: 3840, + height: 2160, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let texture_view = + texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut i = 0; + let mut cache = Some(runtime::user_interface::Cache::default()); + + bencher.iter(|| { + let mut user_interface = runtime::UserInterface::build( + view(i), + viewport.logical_size(), + cache.take().unwrap(), + &mut renderer, + ); + + let _ = user_interface.draw( + &mut renderer, + &Theme::Dark, + &core::renderer::Style { + text_color: Color::WHITE, + }, + mouse::Cursor::Unavailable, + ); + + cache = Some(user_interface.into_cache()); + + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: None, + }); + + renderer.present( + &mut engine, + &device, + &queue, + &mut encoder, + Some(Color::BLACK), + format, + &texture_view, + &viewport, + ); + + let submission = engine.submit(&queue, encoder); + let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); + + i += 1; + }); +} + +fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> { + struct Scene { + n: usize, + } + + impl canvas::Program for Scene { + type State = canvas::Cache; + + fn draw( + &self, + cache: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec> { + vec![cache.draw(renderer, bounds.size(), |frame| { + for i in 0..self.n { + frame.fill_rectangle( + Point::new(0.0, i as f32), + Size::new(10.0, 10.0), + Color::WHITE, + ); + } + + for i in 0..self.n { + frame.fill_text(canvas::Text { + content: i.to_string(), + position: Point::new(0.0, i as f32), + color: Color::BLACK, + size: Pixels::from(16), + line_height: text::LineHeight::default(), + font: Font::DEFAULT, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Basic, + }); + } + })] + } + } + + canvas(Scene { n }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn layered_text<'a, Message: 'a>( + n: usize, +) -> Element<'a, Message, Theme, Renderer> { + stack((0..n).map(|i| text(format!("I am paragraph {i}!")).into())) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn dynamic_text<'a, Message: 'a>( + n: usize, + i: usize, +) -> Element<'a, Message, Theme, Renderer> { + const LOREM_IPSUM: &str = include_str!("ipsum.txt"); + + scrollable( + text(format!( + "{}... Iteration {i}", + std::iter::repeat(LOREM_IPSUM.chars()) + .flatten() + .take(n) + .collect::(), + )) + .size(10), + ) + .into() +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 6e1f5ffb..4ea6b330 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,18 +10,28 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + +[features] +auto-detect-theme = ["dep:dark-light"] +advanced = [] + [dependencies] bitflags.workspace = true +bytes.workspace = true glam.workspace = true log.workspace = true num-traits.workspace = true +once_cell.workspace = true +palette.workspace = true +rustc-hash.workspace = true smol_str.workspace = true thiserror.workspace = true web-time.workspace = true -xxhash-rust.workspace = true -palette.workspace = true -palette.optional = true +dark-light.workspace = true +dark-light.optional = true serde.workspace = true serde.optional = true diff --git a/core/src/angle.rs b/core/src/angle.rs index 30ddad83..9c8a9b24 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -1,12 +1,75 @@ use crate::{Point, Rectangle, Vector}; use std::f32::consts::{FRAC_PI_2, PI}; -use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign}; /// Degrees #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Degrees(pub f32); +impl Degrees { + /// The range of degrees of a circle. + pub const RANGE: RangeInclusive = Self(0.0)..=Self(360.0); +} + +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) + } +} + +impl From for Degrees { + fn from(degrees: f32) -> Self { + Self(degrees) + } +} + +impl From for Degrees { + fn from(degrees: u8) -> Self { + Self(f32::from(degrees)) + } +} + +impl From for f32 { + fn from(degrees: Degrees) -> Self { + degrees.0 + } +} + +impl From for f64 { + fn from(degrees: Degrees) -> Self { + Self::from(degrees.0) + } +} + +impl Mul for Degrees { + type Output = Degrees; + + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl num_traits::FromPrimitive for Degrees { + fn from_i64(n: i64) -> Option { + Some(Self(n as f32)) + } + + fn from_u64(n: u64) -> Option { + Some(Self(n as f32)) + } + + fn from_f64(n: f64) -> Option { + Some(Self(n as f32)) + } +} + /// Radians #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Radians(pub f32); @@ -53,6 +116,12 @@ impl From for Radians { } } +impl From for f32 { + fn from(radians: Radians) -> Self { + radians.0 + } +} + impl From for f64 { fn from(radians: Radians) -> Self { Self::from(radians.0) @@ -95,6 +164,14 @@ impl Add for Radians { } } +impl Add for Radians { + type Output = Self; + + fn add(self, rhs: Degrees) -> Self::Output { + Self(self.0 + rhs.0.to_radians()) + } +} + impl AddAssign for Radians { fn add_assign(&mut self, rhs: Radians) { self.0 = self.0 + rhs.0; @@ -140,3 +217,23 @@ impl Div for Radians { Self(self.0 / rhs.0) } } + +impl Rem for Radians { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + 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 347c52c0..c8b7cbea 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 64262471..2df24988 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 } } } diff --git a/core/src/color.rs b/core/src/color.rs index a9a0aa55..60fd9a3d 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. @@ -152,6 +151,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 { @@ -203,7 +210,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 { @@ -211,7 +217,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 { @@ -219,7 +224,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 { @@ -227,7 +231,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 { @@ -235,7 +238,6 @@ impl From for Srgb { } } -#[cfg(feature = "palette")] #[cfg(test)] mod tests { use super::*; diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 6bbedc7a..19642716 100644 --- a/core/src/content_fit.rs +++ b/core/src/content_fit.rs @@ -1,6 +1,8 @@ //! Control the fit of some content (like an image) within a space. use crate::Size; +use std::fmt; + /// The strategy used to fit the contents of a widget to its bounding box. /// /// Each variant of this enum is a strategy that can be applied for resolving @@ -11,7 +13,7 @@ use crate::Size; /// in CSS, see [Mozilla's docs][1], or run the `tour` example /// /// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default)] pub enum ContentFit { /// Scale as big as it can be without needing to crop or hide parts. /// @@ -23,6 +25,7 @@ pub enum ContentFit { /// This is a great fit for when you need to display an image without losing /// any part of it, particularly when the image itself is the focus of the /// screen. + #[default] Contain, /// Scale the image to cover all of the bounding box, cropping if needed. @@ -117,3 +120,15 @@ impl ContentFit { } } } + +impl fmt::Display for ContentFit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ContentFit::Contain => "Contain", + ContentFit::Cover => "Cover", + ContentFit::Fill => "Fill", + ContentFit::None => "None", + ContentFit::ScaleDown => "Scale Down", + }) + } +} diff --git a/core/src/element.rs b/core/src/element.rs index 8eea90ca..7d918a2e 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, ()>; /// # - /// # 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; + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; /// # - /// # pub mod widget { - /// # pub struct Row { - /// # _t: std::marker::PhantomData, - /// # } + /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>; /// # - /// # impl Row { - /// # pub fn new() -> Self { - /// # unimplemented!() - /// # } - /// # - /// # pub fn spacing(mut self, _: u32) -> Self { - /// # unimplemented!() - /// # } - /// # - /// # 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 4711b044..ccae0bce 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/hasher.rs b/core/src/hasher.rs index a13d78af..13180e41 100644 --- a/core/src/hasher.rs +++ b/core/src/hasher.rs @@ -1,7 +1,7 @@ /// The hasher used to compare layouts. #[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways. #[derive(Default)] -pub struct Hasher(xxhash_rust::xxh3::Xxh3); +pub struct Hasher(rustc_hash::FxHasher); impl core::hash::Hasher for Hasher { fn write(&mut self, bytes: &[u8]) { diff --git a/core/src/image.rs b/core/src/image.rs index e5fdcd83..82ecdd0f 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,15 +1,45 @@ //! Load and draw raster graphics. -use crate::{Hasher, Rectangle, Size}; +pub use bytes::Bytes; -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; +use crate::{Radians, Rectangle, Size}; + +use rustc_hash::FxHasher; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; /// A handle of some image data. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Handle { - id: u64, - data: Data, +#[derive(Clone, PartialEq, Eq)] +pub enum Handle { + /// A file handle. The image data will be read + /// from the file path. + /// + /// Use [`from_path`] to create this variant. + /// + /// [`from_path`]: Self::from_path + Path(Id, PathBuf), + + /// A handle pointing to some encoded image bytes in-memory. + /// + /// Use [`from_bytes`] to create this variant. + /// + /// [`from_bytes`]: Self::from_bytes + Bytes(Id, Bytes), + + /// A handle pointing to decoded image pixels in RGBA format. + /// + /// Use [`from_rgba`] to create this variant. + /// + /// [`from_rgba`]: Self::from_rgba + Rgba { + /// The id of this handle. + id: Id, + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Bytes, + }, } impl Handle { @@ -17,56 +47,48 @@ impl Handle { /// /// Makes an educated guess about the image format by examining the data in the file. pub fn from_path>(path: T) -> Handle { - Self::from_data(Data::Path(path.into())) + let path = path.into(); + + Self::Path(Id::path(&path), path) } - /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec` of RGBA - /// pixels. - /// - /// This is useful if you have already decoded your image. - pub fn from_pixels( - width: u32, - height: u32, - pixels: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Rgba { - width, - height, - pixels: Bytes::new(pixels), - }) - } - - /// Creates an image [`Handle`] containing the image data directly. + /// Creates an image [`Handle`] containing the encoded image data directly. /// /// Makes an educated guess about the image format by examining the given data. /// /// This is useful if you already have your image loaded in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory( - bytes: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Bytes(Bytes::new(bytes))) + pub fn from_bytes(bytes: impl Into) -> Handle { + Self::Bytes(Id::unique(), bytes.into()) } - fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data, + /// Creates an image [`Handle`] containing the decoded image pixels directly. + /// + /// This function expects the pixel data to be provided as a collection of [`Bytes`] + /// of RGBA pixels. Therefore, the length of the pixel data should always be + /// `width * height * 4`. + /// + /// This is useful if you have already decoded your image. + pub fn from_rgba( + width: u32, + height: u32, + pixels: impl Into, + ) -> Handle { + Self::Rgba { + id: Id::unique(), + width, + height, + pixels: pixels.into(), } } /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the image [`Data`]. - pub fn data(&self) -> &Data { - &self.data + pub fn id(&self) -> Id { + match self { + Handle::Path(id, _) + | Handle::Bytes(id, _) + | Handle::Rgba { id, .. } => *id, + } } } @@ -79,93 +101,49 @@ where } } -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// A wrapper around raw image data. -/// -/// It behaves like a `&[u8]`. -#[derive(Clone)] -pub struct Bytes(Arc + Send + Sync + 'static>); - -impl Bytes { - /// Creates new [`Bytes`] around `data`. - pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { - Self(Arc::new(data)) - } -} - -impl std::fmt::Debug for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().as_ref().fmt(f) - } -} - -impl std::hash::Hash for Bytes { - fn hash(&self, state: &mut H) { - self.0.as_ref().as_ref().hash(state); - } -} - -impl PartialEq for Bytes { - fn eq(&self, other: &Self) -> bool { - let a = self.as_ref(); - let b = other.as_ref(); - core::ptr::eq(a, b) || a == b - } -} - -impl Eq for Bytes {} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -impl std::ops::Deref for Bytes { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -/// The data of a raster image. -#[derive(Clone, PartialEq, Eq, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - Bytes(Bytes), - - /// Decoded image pixels in RGBA format. - Rgba { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Bytes, - }, -} - -impl std::fmt::Debug for Data { +impl std::fmt::Debug for Handle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Rgba { width, height, .. } => { + Self::Path(_, path) => write!(f, "Path({path:?})"), + Self::Bytes(_, _) => write!(f, "Bytes(...)"), + Self::Rgba { width, height, .. } => { write!(f, "Pixels({width} * {height})") } } } } +/// The unique identifier of some [`Handle`] data. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(_Id); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum _Id { + Unique(u64), + Hash(u64), +} + +impl Id { + fn unique() -> Self { + use std::sync::atomic::{self, AtomicU64}; + + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed))) + } + + fn path(path: impl AsRef) -> Self { + let hash = { + let mut hasher = FxHasher::default(); + path.as_ref().hash(&mut hasher); + + hasher.finish() + }; + + Self(_Id::Hash(hash)) + } +} + /// Image filtering strategy. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum FilterMethod { @@ -183,17 +161,19 @@ pub trait Renderer: crate::Renderer { /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] /// /// [`Handle`]: Self::Handle - type Handle: Clone + Hash; + type Handle: Clone; /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> Size; + fn measure_image(&self, handle: &Self::Handle) -> Size; /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. - fn draw( + fn draw_image( &mut self, handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, + rotation: Radians, + opacity: f32, ); } diff --git a/core/src/layout.rs b/core/src/layout.rs index 95720aba..98d05602 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -54,7 +54,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - pub fn children(self) -> impl Iterator> { + pub fn children(self) -> impl DoubleEndedIterator> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 40bd7123..dcb4d8de 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/lib.rs b/core/src/lib.rs index e46e8726..32156441 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -9,14 +9,6 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rust_2018_idioms, - rustdoc::broken_intra_doc_links -)] pub mod alignment; pub mod border; pub mod clipboard; @@ -31,6 +23,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; @@ -41,12 +34,12 @@ mod background; mod color; mod content_fit; mod element; -mod hasher; mod length; mod padding; mod pixels; mod point; mod rectangle; +mod rotation; mod shadow; mod shell; mod size; @@ -64,7 +57,6 @@ pub use element::Element; pub use event::Event; pub use font::Font; pub use gradient::Gradient; -pub use hasher::Hasher; pub use layout::Layout; pub use length::Length; pub use overlay::Overlay; @@ -73,10 +65,12 @@ pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; +pub use rotation::Rotation; 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/mouse/interaction.rs b/core/src/mouse/interaction.rs index 6ad66229..065eb8e7 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -3,6 +3,7 @@ #[allow(missing_docs)] pub enum Interaction { #[default] + None, Idle, Pointer, Grab, diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 03076a30..3a57fe16 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -79,7 +79,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns true if the cursor is over the [`Overlay`]. diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index c1c2eeac..1556e072 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,6 +1,6 @@ -use crate::{Point, Size, Vector}; +use crate::{Point, Radians, Size, Vector}; -/// A rectangle. +/// An axis-aligned rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Rectangle { /// X coordinate of the top-left corner. @@ -16,24 +16,32 @@ pub struct Rectangle { pub height: T, } -impl Rectangle { - /// Creates a new [`Rectangle`] with its top-left corner in the given - /// [`Point`] and with the provided [`Size`]. - pub fn new(top_left: Point, size: Size) -> Self { +impl Rectangle +where + T: Default, +{ + /// Creates a new [`Rectangle`] with its top-left corner at the origin + /// and with the provided [`Size`]. + pub fn with_size(size: Size) -> Self { Self { - x: top_left.x, - y: top_left.y, + x: T::default(), + y: T::default(), width: size.width, height: size.height, } } +} - /// Creates a new [`Rectangle`] with its top-left corner at the origin - /// and with the provided [`Size`]. - pub fn with_size(size: Size) -> Self { +impl Rectangle { + /// A rectangle starting at [`Point::ORIGIN`] with infinite width and height. + pub const INFINITE: Self = Self::new(Point::ORIGIN, Size::INFINITY); + + /// Creates a new [`Rectangle`] with its top-left corner in the given + /// [`Point`] and with the provided [`Size`]. + pub const fn new(top_left: Point, size: Size) -> Self { Self { - x: 0.0, - y: 0.0, + x: top_left.x, + y: top_left.y, width: size.width, height: size.height, } @@ -139,13 +147,20 @@ impl Rectangle { } /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. - pub fn snap(self) -> Rectangle { - Rectangle { + pub fn snap(self) -> Option> { + let width = self.width as u32; + let height = self.height as u32; + + if width < 1 || height < 1 { + return None; + } + + Some(Rectangle { x: self.x as u32, y: self.y as u32, - width: self.width as u32, - height: self.height as u32, - } + width, + height, + }) } /// Expands the [`Rectangle`] a given amount. @@ -157,6 +172,18 @@ impl Rectangle { height: self.height + amount * 2.0, } } + + /// Rotates the [`Rectangle`] and returns the smallest [`Rectangle`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Self { + let size = self.size().rotate(rotation); + let position = Point::new( + self.center_x() - size.width / 2.0, + self.center_y() - size.height / 2.0, + ); + + Self::new(position, size) + } } impl std::ops::Mul for Rectangle { @@ -212,3 +239,19 @@ where } } } + +impl std::ops::Mul> for Rectangle +where + T: std::ops::Mul + Copy, +{ + type Output = Rectangle; + + fn mul(self, scale: Vector) -> Self { + Rectangle { + x: self.x * scale.x, + y: self.y * scale.y, + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 1139b41c..a2785ae8 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -2,26 +2,47 @@ #[cfg(debug_assertions)] mod null; -#[cfg(debug_assertions)] -pub use null::Null; - use crate::{ Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector, }; /// A component that can be used by widgets to draw themselves on a screen. -pub trait Renderer: Sized { +pub trait Renderer { + /// Starts recording a new layer. + fn start_layer(&mut self, bounds: Rectangle); + + /// Ends recording a new layer. + /// + /// The new layer will clip its contents to the provided `bounds`. + fn end_layer(&mut self); + /// Draws the primitives recorded in the given closure in a new layer. /// /// The layer will clip its contents to the provided `bounds`. - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)); + fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { + self.start_layer(bounds); + f(self); + self.end_layer(); + } + + /// Starts recording with a new [`Transformation`]. + fn start_transformation(&mut self, transformation: Transformation); + + /// Ends recording a new layer. + /// + /// The new layer will clip its contents to the provided `bounds`. + fn end_transformation(&mut self); /// Applies a [`Transformation`] to the primitives recorded in the given closure. fn with_transformation( &mut self, transformation: Transformation, f: impl FnOnce(&mut Self), - ); + ) { + self.start_transformation(transformation); + f(self); + self.end_transformation(); + } /// Applies a translation to the primitives recorded in the given closure. fn with_translation( diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 83688ff7..e8709dbc 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -1,34 +1,21 @@ use crate::alignment; +use crate::image; use crate::renderer::{self, Renderer}; +use crate::svg; use crate::text::{self, Text}; use crate::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, }; -use std::borrow::Cow; +impl Renderer for () { + fn start_layer(&mut self, _bounds: Rectangle) {} -/// A renderer that does nothing. -/// -/// It can be useful if you are writing tests! -#[derive(Debug, Clone, Copy, Default)] -pub struct Null; + fn end_layer(&mut self) {} -impl Null { - /// Creates a new [`Null`] renderer. - pub fn new() -> Null { - Null - } -} + fn start_transformation(&mut self, _transformation: Transformation) {} -impl Renderer for Null { - fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} - - fn with_transformation( - &mut self, - _transformation: Transformation, - _f: impl FnOnce(&mut Self), - ) { - } + fn end_transformation(&mut self) {} fn clear(&mut self) {} @@ -40,7 +27,7 @@ impl Renderer for Null { } } -impl text::Renderer for Null { +impl text::Renderer for () { type Font = Font; type Paragraph = (); type Editor = (); @@ -57,8 +44,6 @@ impl text::Renderer for Null { Pixels(16.0) } - fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn fill_paragraph( &mut self, _paragraph: &Self::Paragraph, @@ -79,7 +64,7 @@ impl text::Renderer for Null { fn fill_text( &mut self, - _paragraph: Text<'_, Self::Font>, + _paragraph: Text, _position: Point, _color: Color, _clip_bounds: Rectangle, @@ -90,11 +75,11 @@ impl text::Renderer for Null { impl text::Paragraph for () { type Font = Font; - fn with_text(_text: Text<'_, Self::Font>) -> Self {} + fn with_text(_text: Text<&str>) -> Self {} fn resize(&mut self, _new_bounds: Size) {} - fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference { + fn compare(&self, _text: Text<&str>) -> text::Difference { text::Difference::None } @@ -174,3 +159,37 @@ impl text::Editor for () { ) { } } + +impl image::Renderer for () { + type Handle = (); + + fn measure_image(&self, _handle: &Self::Handle) -> Size { + Size::default() + } + + fn draw_image( + &mut self, + _handle: Self::Handle, + _filter_method: image::FilterMethod, + _bounds: Rectangle, + _rotation: Radians, + _opacity: f32, + ) { + } +} + +impl svg::Renderer for () { + fn measure_svg(&self, _handle: &svg::Handle) -> Size { + Size::default() + } + + fn draw_svg( + &mut self, + _handle: svg::Handle, + _color: Option, + _bounds: Rectangle, + _rotation: Radians, + _opacity: f32, + ) { + } +} diff --git a/core/src/rotation.rs b/core/src/rotation.rs new file mode 100644 index 00000000..afa8d79e --- /dev/null +++ b/core/src/rotation.rs @@ -0,0 +1,72 @@ +//! Control the rotation of some content (like an image) within a space. +use crate::{Degrees, Radians, Size}; + +/// The strategy used to rotate the content. +/// +/// This is used to control the behavior of the layout when the content is rotated +/// by a certain angle. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Rotation { + /// The element will float while rotating. The layout will be kept exactly as it was + /// before the rotation. + /// + /// This is especially useful when used for animations, as it will avoid the + /// layout being shifted or resized when smoothly i.e. an icon. + /// + /// This is the default. + Floating(Radians), + /// The element will be solid while rotating. The layout will be adjusted to fit + /// the rotated content. + /// + /// This allows you to rotate an image and have the layout adjust to fit the new + /// size of the image. + Solid(Radians), +} + +impl Rotation { + /// Returns the angle of the [`Rotation`] in [`Radians`]. + pub fn radians(self) -> Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Returns a mutable reference to the angle of the [`Rotation`] in [`Radians`]. + pub fn radians_mut(&mut self) -> &mut Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Returns the angle of the [`Rotation`] in [`Degrees`]. + pub fn degrees(self) -> Degrees { + Degrees(self.radians().0.to_degrees()) + } + + /// Applies the [`Rotation`] to the given [`Size`], returning + /// the minimum [`Size`] containing the rotated one. + pub fn apply(self, size: Size) -> Size { + match self { + Self::Floating(_) => size, + Self::Solid(rotation) => size.rotate(rotation), + } + } +} + +impl Default for Rotation { + fn default() -> Self { + Self::Floating(Radians(0.0)) + } +} + +impl From for Rotation { + fn from(radians: Radians) -> Self { + Self::Floating(radians) + } +} + +impl From for Rotation { + fn from(radians: f32) -> Self { + Self::Floating(Radians(radians)) + } +} diff --git a/core/src/size.rs b/core/src/size.rs index 90e50d13..d7459355 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,7 +1,7 @@ -use crate::Vector; +use crate::{Radians, Vector}; /// An amount of space in 2 dimensions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Size { /// The width. pub width: T, @@ -51,22 +51,35 @@ impl Size { height: self.height + other.height, } } + + /// Rotates the given [`Size`] and returns the minimum [`Size`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Size { + let radians = f32::from(rotation); + + Size { + width: (self.width * radians.cos()).abs() + + (self.height * radians.sin()).abs(), + height: (self.width * radians.sin()).abs() + + (self.height * radians.cos()).abs(), + } + } } -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 +87,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 { @@ -96,3 +112,31 @@ impl std::ops::Sub for Size { } } } + +impl std::ops::Mul for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Size; + + fn mul(self, rhs: T) -> Self::Output { + Size { + width: self.width * rhs, + height: self.height * rhs, + } + } +} + +impl std::ops::Mul> for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Size; + + fn mul(self, scale: Vector) -> Self::Output { + Size { + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs index d63e3c95..946b8156 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -1,6 +1,7 @@ //! Load and draw vector graphics. -use crate::{Color, Hasher, Rectangle, Size}; +use crate::{Color, Radians, Rectangle, Size}; +use rustc_hash::FxHasher; use std::borrow::Cow; use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; @@ -30,7 +31,7 @@ impl Handle { } fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); + let mut hasher = FxHasher::default(); data.hash(&mut hasher); Handle { @@ -91,8 +92,15 @@ impl std::fmt::Debug for Data { /// [renderer]: crate::renderer pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an SVG for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> Size; + fn measure_svg(&self, handle: &Handle) -> Size; /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); + fn draw_svg( + &mut self, + handle: Handle, + color: Option, + bounds: Rectangle, + rotation: Radians, + opacity: f32, + ); } diff --git a/core/src/text.rs b/core/src/text.rs index edef79c2..b30feae0 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -11,14 +11,13 @@ pub use paragraph::Paragraph; use crate::alignment; use crate::{Color, Pixels, Point, Rectangle, Size}; -use std::borrow::Cow; use std::hash::{Hash, Hasher}; /// A paragraph. #[derive(Debug, Clone, Copy)] -pub struct Text<'a, Font> { +pub struct Text { /// The content of the paragraph. - pub content: &'a str, + pub content: Content, /// The bounds of the paragraph. pub bounds: Size, @@ -192,9 +191,6 @@ pub trait Renderer: crate::Renderer { /// Returns the default size of [`Text`]. fn default_size(&self) -> Pixels; - /// Loads a [`Self::Font`] from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( @@ -219,7 +215,7 @@ pub trait Renderer: crate::Renderer { /// [`Color`]. fn fill_text( &mut self, - text: Text<'_, Self::Font>, + text: Text, position: Point, color: Color, clip_bounds: Rectangle, diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index de1fb74d..8ff04015 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -8,14 +8,14 @@ pub trait Paragraph: Sized + Default { type Font: Copy + PartialEq; /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn with_text(text: Text<'_, Self::Font>) -> Self; + fn with_text(text: Text<&str, Self::Font>) -> Self; /// Lays out the [`Paragraph`] with some new boundaries. fn resize(&mut self, new_bounds: Size); /// Compares the [`Paragraph`] with some desired [`Text`] and returns the /// [`Difference`]. - fn compare(&self, text: Text<'_, Self::Font>) -> Difference; + fn compare(&self, text: Text<&str, Self::Font>) -> Difference; /// Returns the horizontal alignment of the [`Paragraph`]. fn horizontal_alignment(&self) -> alignment::Horizontal; @@ -35,7 +35,7 @@ pub trait Paragraph: Sized + Default { fn grapheme_position(&self, line: usize, index: usize) -> Option; /// Updates the [`Paragraph`] to match the given [`Text`], if needed. - fn update(&mut self, text: Text<'_, Self::Font>) { + fn update(&mut self, text: Text<&str, Self::Font>) { match self.compare(text) { Difference::None => {} Difference::Bounds => { diff --git a/core/src/theme.rs b/core/src/theme.rs new file mode 100644 index 00000000..6b2c04da --- /dev/null +++ b/core/src/theme.rs @@ -0,0 +1,248 @@ +//! 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)] +pub enum Theme { + /// The built-in light variant. + 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 Default for Theme { + fn default() -> Self { + #[cfg(feature = "auto-detect-theme")] + { + use once_cell::sync::Lazy; + + static DEFAULT: Lazy = + Lazy::new(|| match dark_light::detect() { + dark_light::Mode::Dark => Theme::Dark, + dark_light::Mode::Light | dark_light::Mode::Default => { + Theme::Light + } + }); + + DEFAULT.clone() + } + + #[cfg(not(feature = "auto-detect-theme"))] + Theme::Light + } +} + +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 95% rename from style/src/theme/palette.rs rename to core/src/theme/palette.rs index 74a8253a..ec54fb9c 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; @@ -277,6 +277,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`]. @@ -380,6 +391,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 { @@ -599,10 +614,15 @@ fn mix(a: Color, b: Color, factor: f32) -> Color { fn readable(background: Color, text: Color) -> Color { if is_readable(background, text) { text - } else if is_dark(background) { - Color::WHITE } else { - Color::BLACK + let white_contrast = relative_contrast(background, Color::WHITE); + let black_contrast = relative_contrast(background, Color::BLACK); + + if white_contrast >= black_contrast { + Color::WHITE + } else { + Color::BLACK + } } } @@ -617,6 +637,13 @@ fn is_readable(a: Color, b: Color) -> bool { a_srgb.has_enhanced_contrast_text(b_srgb) } +fn relative_contrast(a: Color, b: Color) -> f32 { + let a_srgb = Rgb::from(a); + let b_srgb = Rgb::from(b); + + a_srgb.relative_contrast(b_srgb) +} + fn to_hsl(color: Color) -> Hsl { Hsl::from_color(Rgb::from(color)) } diff --git a/core/src/transformation.rs b/core/src/transformation.rs index b2c488b0..74183147 100644 --- a/core/src/transformation.rs +++ b/core/src/transformation.rs @@ -42,6 +42,12 @@ impl Transformation { } } +impl Default for Transformation { + fn default() -> Self { + Transformation::IDENTITY + } +} + impl Mul for Transformation { type Output = Self; diff --git a/core/src/vector.rs b/core/src/vector.rs index 1380c3b3..049e648f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,6 +18,9 @@ impl Vector { impl Vector { /// The zero [`Vector`]. pub const ZERO: Self = Self::new(0.0, 0.0); + + /// The unit [`Vector`]. + pub const UNIT: Self = Self::new(0.0, 0.0); } impl std::ops::Add for Vector diff --git a/core/src/widget.rs b/core/src/widget.rs index 58a9f19b..b02e3a4f 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -137,7 +137,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns the overlay of the [`Widget`], if there is any. diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 0796c4e4..f1f0b345 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,10 +18,10 @@ pub use text::{LineHeight, Shaping}; #[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { - content: Cow<'a, str>, + fragment: Fragment<'a>, size: Option, line_height: LineHeight, width: Length, @@ -29,18 +30,18 @@ where vertical_alignment: alignment::Vertical, font: Option, shaping: Shaping, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. - pub fn new(content: impl Into>) -> Self { + pub fn new(fragment: impl IntoFragment<'a>) -> Self { Text { - content: content.into(), + fragment: fragment.into_fragment(), size: None, line_height: LineHeight::default(), font: None, @@ -49,7 +50,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Basic, - style: Default::default(), + class: Theme::default(), } } @@ -73,12 +74,6 @@ where self } - /// Sets the style of the [`Text`]. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); - self - } - /// Sets the width of the [`Text`] boundaries. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -114,6 +109,42 @@ where self.shaping = shaping; self } + + /// Sets the style of the [`Text`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the [`Color`] of the [`Text`]. + pub fn color(self, color: impl Into) -> Self + where + Theme::Class<'a>: From>, + { + self.color_maybe(Some(color)) + } + + /// Sets the [`Color`] of the [`Text`], if `Some`. + pub fn color_maybe(self, color: Option>) -> Self + where + Theme::Class<'a>: From>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the style class of the [`Text`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into>) -> Self { + self.class = class.into(); + self + } } /// The internal state of a [`Text`] widget. @@ -123,7 +154,7 @@ pub struct State(P); impl<'a, Message, Theme, Renderer> Widget for Text<'a, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -153,7 +184,7 @@ where limits, self.width, self.height, - &self.content, + &self.fragment, self.line_height, self.size, self.font, @@ -168,21 +199,15 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, _cursor_position: mouse::Cursor, viewport: &Rectangle, ) { let state = tree.state.downcast_ref::>(); + let style = theme.style(&self.class); - draw( - renderer, - style, - layout, - state, - theme.appearance(self.style.clone()), - viewport, - ); + draw(renderer, defaults, layout, state, style, viewport); } } @@ -242,7 +267,7 @@ pub fn draw( style: &renderer::Style, layout: Layout<'_>, state: &State, - appearance: Appearance, + appearance: Style, viewport: &Rectangle, ) where Renderer: text::Renderer, @@ -273,7 +298,7 @@ pub fn draw( impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -283,30 +308,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: Catalog + 'a, Renderer: text::Renderer, { fn from(content: &'a str) -> Self { @@ -317,7 +321,7 @@ where impl<'a, Message, Theme, Renderer> From<&'a str> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from(content: &'a str) -> Self { @@ -325,20 +329,118 @@ 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 { +pub struct Style { /// The [`Color`] of the text. /// /// The default, `None`, means using the inherited color. pub color: Option, } + +/// The theme catalog of a [`Text`]. +pub trait Catalog: Sized { + /// The item class of this [`Catalog`]. + type Class<'a>; + + /// The default class produced by this [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, item: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`Text`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|_theme| Style::default()) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// A fragment of [`Text`]. +/// +/// This is just an alias to a string that may be either +/// borrowed or owned. +pub type Fragment<'a> = Cow<'a, str>; + +/// A trait for converting a value to some text [`Fragment`]. +pub trait IntoFragment<'a> { + /// Converts the value to some text [`Fragment`]. + fn into_fragment(self) -> Fragment<'a>; +} + +impl<'a> IntoFragment<'a> for Fragment<'a> { + fn into_fragment(self) -> Fragment<'a> { + self + } +} + +impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a str { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self) + } +} + +impl<'a> IntoFragment<'a> for &'a String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Borrowed(self.as_str()) + } +} + +impl<'a> IntoFragment<'a> for String { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self) + } +} + +macro_rules! into_fragment { + ($type:ty) => { + impl<'a> IntoFragment<'a> for $type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + + impl<'a> IntoFragment<'a> for &$type { + fn into_fragment(self) -> Fragment<'a> { + Fragment::Owned(self.to_string()) + } + } + }; +} + +into_fragment!(char); +into_fragment!(bool); + +into_fragment!(u8); +into_fragment!(u16); +into_fragment!(u32); +into_fragment!(u64); +into_fragment!(u128); +into_fragment!(usize); + +into_fragment!(i8); +into_fragment!(i16); +into_fragment!(i32); +into_fragment!(i64); +into_fragment!(i128); +into_fragment!(isize); + +into_fragment!(f32); +into_fragment!(f64); diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs index 8a59e83c..b0c000d6 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/core/src/window/settings/wasm.rs b/core/src/window/settings/wasm.rs index 8e0f1bbc..30e60b6a 100644 --- a/core/src/window/settings/wasm.rs +++ b/core/src/window/settings/wasm.rs @@ -1,11 +1,21 @@ //! Platform specific settings for WebAssembly. /// The platform specific window settings of an application. -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PlatformSpecific { /// The identifier of a DOM element that will be replaced with the /// application. /// /// If set to `None`, the application will be appended to the HTML body. + /// + /// By default, it is set to `"iced"`. pub target: Option, } + +impl Default for PlatformSpecific { + fn default() -> Self { + Self { + target: Some(String::from("iced")), + } + } +} diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 5f63fb90..4e3e0a61 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -15,7 +15,6 @@ enable = ["dep:iced_sentinel", "dep:once_cell"] [dependencies] iced_core.workspace = true -iced_style.workspace = true iced_sentinel.workspace = true iced_sentinel.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index cabe4440..4fc9a9a1 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -1,8 +1,7 @@ pub use iced_core as core; -pub use iced_style as style; +use crate::core::theme; use crate::core::window; -use crate::style::theme; pub use internal::Timer; @@ -52,9 +51,9 @@ pub fn skip_next_timing() { #[cfg(feature = "enable")] mod internal { + use crate::core::theme; use crate::core::time::{Instant, SystemTime}; use crate::core::window; - use crate::style::theme; use iced_sentinel::client::{self, Client}; use iced_sentinel::timing::{self, Timing}; diff --git a/docs/logo.svg b/docs/logo.svg index ff4eb3a7..aa1924c2 100644 --- a/docs/logo.svg +++ b/docs/logo.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/docs/redirect.html b/docs/redirect.html new file mode 100644 index 00000000..7b2cef51 --- /dev/null +++ b/docs/redirect.html @@ -0,0 +1,13 @@ + + + + + + + Redirecting... + + + +

If you are not redirected automatically, follow this link.

+ + diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 6a68cca1..4576404f 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 56cb23ba..29df3eeb 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,12 +1,13 @@ //! 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; +use iced::widget::{button, container, horizontal_space, hover}; +use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { - Example::run(Settings { - antialiasing: true, - ..Settings::default() - }) + iced::program("Bezier Tool - Iced", Example::update, Example::view) + .theme(|_| Theme::CatppuccinMocha) + .antialiasing(true) + .run() } #[derive(Default)] @@ -21,17 +22,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) => { @@ -46,14 +37,22 @@ impl Sandbox for Example { } fn view(&self) -> Element { - column![ - text("Bezier tool example").width(Length::Shrink).size(50), + container(hover( self.bezier.view(&self.curves).map(Message::AddCurve), - button("Clear").padding(8).on_press(Message::Clear), - ] + if self.curves.is_empty() { + container(horizontal_space()) + } else { + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear), + ) + .padding(10) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right) + }, + )) .padding(20) - .spacing(20) - .align_items(Alignment::Center) .into() } } @@ -148,27 +147,24 @@ mod bezier { &self, state: &Self::State, renderer: &Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec { - let content = self.state.cache.draw( - renderer, - bounds.size(), - |frame: &mut Frame| { - Curve::draw_all(self.curves, frame); + let content = + self.state.cache.draw(renderer, bounds.size(), |frame| { + Curve::draw_all(self.curves, frame, theme); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default().with_width(2.0), + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), ); - }, - ); + }); if let Some(pending) = state { - let pending_curve = pending.draw(renderer, bounds, cursor); - - vec![content, pending_curve] + vec![content, pending.draw(renderer, theme, bounds, cursor)] } else { vec![content] } @@ -196,7 +192,7 @@ mod bezier { } impl Curve { - fn draw_all(curves: &[Curve], frame: &mut Frame) { + fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { let curves = Path::new(|p| { for curve in curves { p.move_to(curve.from); @@ -204,7 +200,12 @@ mod bezier { } }); - frame.stroke(&curves, Stroke::default().with_width(2.0)); + frame.stroke( + &curves, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } } @@ -218,6 +219,7 @@ mod bezier { fn draw( &self, renderer: &Renderer, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Geometry { @@ -227,7 +229,12 @@ mod bezier { match *self { Pending::One { from } => { let line = Path::line(from, cursor_position); - frame.stroke(&line, Stroke::default().with_width(2.0)); + frame.stroke( + &line, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } Pending::Two { from, to } => { let curve = Curve { @@ -236,7 +243,7 @@ mod bezier { control: cursor_position, }; - Curve::draw_all(&[curve], &mut frame); + Curve::draw_all(&[curve], &mut frame, theme); } }; } diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 834a8f5c..bec4a954 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::widget::{center, checkbox, column, row, text}; +use iced::{Element, Font}; 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); @@ -91,11 +68,6 @@ impl Application for Example { let content = column![default_checkbox, checkboxes, custom_checkbox].spacing(20); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } } diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 2d3d5908..dc2e5382 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -10,3 +10,4 @@ iced.workspace = true iced.features = ["canvas", "tokio", "debug"] time = { version = "0.3", features = ["local-offset"] } +tracing-subscriber = "0.3" diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 13252526..d717db36 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,17 +1,20 @@ -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() - }) + tracing_subscriber::fmt::init(); + + iced::program("Clock - Iced", Clock::update, Clock::view) + .subscription(Clock::subscription) + .theme(Clock::theme) + .antialiasing(true) + .run() } struct Clock { @@ -24,28 +27,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 +39,6 @@ impl Application for Clock { } } } - - Command::none() } fn view(&self) -> Element { @@ -80,6 +61,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 +85,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 +109,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 +118,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 +137,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 +169,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 2da6c6ed..bf9bff19 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 a5fd46e0..d9325edb 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 fcf5feaa..ff759ab4 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, + center, column, combo_box, 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) => { @@ -74,12 +68,13 @@ impl Sandbox for Example { .align_items(Alignment::Center) .spacing(10); - container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(scrollable(content)).into() + } +} + +impl Default for Example { + fn default() -> Self { + Example::new() } } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 4670824d..5625f12a 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::widget::center; +use iced::Element; 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) => { @@ -37,10 +27,8 @@ impl Sandbox for Component { } fn view(&self) -> Element { - container(numeric_input(self.value, Message::NumericInputChanged)) + center(numeric_input(self.value, Message::NumericInputChanged)) .padding(20) - .height(Length::Fill) - .center_y() .into() } } @@ -81,7 +69,10 @@ mod numeric_input { } } - impl Component for NumericInput { + impl Component for NumericInput + where + Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, + { type State = (); type Event = Event; @@ -111,7 +102,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) @@ -152,8 +143,10 @@ mod numeric_input { } } - impl<'a, Message> From> for Element<'a, Message> + impl<'a, Message, Theme> From> + for Element<'a, Message, Theme> where + Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static, Message: 'a, { fn from(numeric_input: NumericInput) -> Self { diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 13dcbf86..0dd7a976 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 f64379fa..b3eee218 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -81,13 +81,11 @@ mod quad { } } -use iced::widget::{column, container, slider, text}; -use iced::{ - Alignment, Color, Element, Length, Sandbox, Settings, Shadow, Vector, -}; +use iced::widget::{center, column, slider, text}; +use iced::{Alignment, Color, Element, 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 { @@ -195,11 +187,12 @@ impl Sandbox for Example { .max_width(500) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).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 9e8da3ba..463b2df9 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::widget::{center, checkbox, column, 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(), - ) +impl IcedCubes { + fn new() -> Self { + Self { + start: Instant::now(), + scene: Scene::new(), + } } - fn title(&self) -> String { - "Iced Cubes".to_string() - } - - 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", @@ -139,19 +123,20 @@ impl Application for IcedCubes { let shader = shader(&self.scene).width(Length::Fill).height(Length::Fill); - container(column![shader, controls].align_items(Alignment::Center)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(column![shader, controls].align_items(Alignment::Center)).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_shader/src/scene.rs b/examples/custom_shader/src/scene.rs index a35efdd9..5fa42188 100644 --- a/examples/custom_shader/src/scene.rs +++ b/examples/custom_shader/src/scene.rs @@ -9,8 +9,8 @@ use pipeline::cube::{self, Cube}; use iced::mouse; use iced::time::Duration; -use iced::widget::shader; -use iced::{Color, Rectangle, Size}; +use iced::widget::shader::{self, Viewport}; +use iced::{Color, Rectangle}; use glam::Vec3; use rand::Rng; @@ -130,25 +130,29 @@ impl Primitive { impl shader::Primitive for Primitive { fn prepare( &self, - format: wgpu::TextureFormat, device: &wgpu::Device, queue: &wgpu::Queue, - _bounds: Rectangle, - target_size: Size, - _scale_factor: f32, + format: wgpu::TextureFormat, storage: &mut shader::Storage, + _bounds: &Rectangle, + viewport: &Viewport, ) { if !storage.has::() { - storage.store(Pipeline::new(device, queue, format, target_size)); + storage.store(Pipeline::new( + device, + queue, + format, + viewport.physical_size(), + )); } let pipeline = storage.get_mut::().unwrap(); - //upload data to GPU + // Upload data to GPU pipeline.update( device, queue, - target_size, + viewport.physical_size(), &self.uniforms, self.cubes.len(), &self.cubes, @@ -157,20 +161,19 @@ impl shader::Primitive for Primitive { fn render( &self, + encoder: &mut wgpu::CommandEncoder, storage: &shader::Storage, target: &wgpu::TextureView, - _target_size: Size, - viewport: Rectangle, - encoder: &mut wgpu::CommandEncoder, + clip_bounds: &Rectangle, ) { - //at this point our pipeline should always be initialized + // At this point our pipeline should always be initialized let pipeline = storage.get::().unwrap(); - //render primitive + // Render primitive pipeline.render( target, encoder, - viewport, + *clip_bounds, self.cubes.len() as u32, self.show_depth_buffer, ); diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 25c0bb39..261dcb81 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, @@ -82,11 +82,11 @@ mod circle { } use circle::circle; -use iced::widget::{column, container, slider, text}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::widget::{center, column, slider, text}; +use iced::{Alignment, Element}; 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) => { @@ -128,11 +122,12 @@ impl Sandbox for Example { .max_width(500) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() + } +} + +impl Default for Example { + fn default() -> Self { + Self::new() } } diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index 3b11cb76..d6cc1e24 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -12,12 +12,6 @@ pub fn file( }) } -#[derive(Debug, Hash, Clone)] -pub struct Download { - id: I, - url: String, -} - async fn download(id: I, state: State) -> ((I, Progress), State) { match state { State::Ready(url) => { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 675e9e26..e031ac44 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, center, column, progress_bar, text, Column}; +use iced::{Alignment, Element, 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(), - ) +impl Example { + fn new() -> Self { + Self { + downloads: vec![Download::new(0)], + last_id: 0, + } } - fn title(&self) -> String { - String::from("Download progress - Iced") - } - - 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 { @@ -83,13 +67,13 @@ impl Application for Example { .spacing(20) .align_items(Alignment::End); - container(downloads) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .padding(20) - .into() + center(downloads).padding(20).into() + } +} + +impl Default for Example { + fn default() -> Self { + Self::new() } } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 53c9cf7c..af05031a 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 { @@ -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) @@ -279,7 +277,7 @@ fn action<'a, Message: Clone + 'a>( label: &'a str, on_press: Option, ) -> Element<'a, Message> { - let action = button(container(content).width(30).center_x()); + let action = button(container(content).center_x().width(30)); if let Some(on_press) = on_press { tooltip( @@ -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 d5d496c7..999ce8ef 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::widget::{button, center, checkbox, 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 => { @@ -104,11 +84,6 @@ impl Application for Events { .push(toggle) .push(exit); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index ec618dc1..2de97e20 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::widget::{button, center, column}; use iced::window; -use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; +use iced::{Alignment, Command, Element}; 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), @@ -60,12 +46,6 @@ impl Application for Exit { .spacing(10) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .center_x() - .center_y() - .into() + center(content).padding(20).into() } } diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml new file mode 100644 index 00000000..e98341d2 --- /dev/null +++ b/examples/ferris/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ferris" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["image", "tokio", "debug"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs new file mode 100644 index 00000000..0400c376 --- /dev/null +++ b/examples/ferris/src/main.rs @@ -0,0 +1,211 @@ +use iced::time::Instant; +use iced::widget::{ + center, checkbox, column, container, image, pick_list, row, slider, text, +}; +use iced::window; +use iced::{ + Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation, + Subscription, Theme, +}; + +pub fn main() -> iced::Result { + iced::program("Ferris - Iced", Image::update, Image::view) + .subscription(Image::subscription) + .theme(|_| Theme::TokyoNight) + .run() +} + +struct Image { + width: f32, + opacity: f32, + rotation: Rotation, + content_fit: ContentFit, + spin: bool, + last_tick: Instant, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + WidthChanged(f32), + OpacityChanged(f32), + RotationStrategyChanged(RotationStrategy), + RotationChanged(Degrees), + ContentFitChanged(ContentFit), + SpinToggled(bool), + RedrawRequested(Instant), +} + +impl Image { + fn update(&mut self, message: Message) { + match message { + Message::WidthChanged(width) => { + self.width = width; + } + Message::OpacityChanged(opacity) => { + self.opacity = opacity; + } + Message::RotationStrategyChanged(strategy) => { + self.rotation = match strategy { + RotationStrategy::Floating => { + Rotation::Floating(self.rotation.radians()) + } + RotationStrategy::Solid => { + Rotation::Solid(self.rotation.radians()) + } + }; + } + Message::RotationChanged(rotation) => { + self.rotation = match self.rotation { + Rotation::Floating(_) => { + Rotation::Floating(rotation.into()) + } + Rotation::Solid(_) => Rotation::Solid(rotation.into()), + }; + } + Message::ContentFitChanged(content_fit) => { + self.content_fit = content_fit; + } + Message::SpinToggled(spin) => { + self.spin = spin; + self.last_tick = Instant::now(); + } + Message::RedrawRequested(now) => { + const ROTATION_SPEED: Degrees = Degrees(360.0); + + let delta = (now - self.last_tick).as_millis() as f32 / 1_000.0; + + *self.rotation.radians_mut() = (self.rotation.radians() + + ROTATION_SPEED * delta) + % (2.0 * Radians::PI); + + self.last_tick = now; + } + } + } + + fn subscription(&self) -> Subscription { + if self.spin { + window::frames().map(Message::RedrawRequested) + } else { + Subscription::none() + } + } + + fn view(&self) -> Element { + let i_am_ferris = column![ + "Hello!", + Element::from( + image(format!( + "{}/../tour/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + .width(self.width) + .content_fit(self.content_fit) + .rotation(self.rotation) + .opacity(self.opacity) + ) + .explain(Color::WHITE), + "I am Ferris!" + ] + .spacing(20) + .align_items(Alignment::Center); + + let fit = row![ + pick_list( + [ + ContentFit::Contain, + ContentFit::Cover, + ContentFit::Fill, + ContentFit::None, + ContentFit::ScaleDown + ], + Some(self.content_fit), + Message::ContentFitChanged + ) + .width(Length::Fill), + pick_list( + [RotationStrategy::Floating, RotationStrategy::Solid], + Some(match self.rotation { + Rotation::Floating(_) => RotationStrategy::Floating, + Rotation::Solid(_) => RotationStrategy::Solid, + }), + Message::RotationStrategyChanged, + ) + .width(Length::Fill), + ] + .spacing(10) + .align_items(Alignment::End); + + let properties = row![ + with_value( + slider(100.0..=500.0, self.width, Message::WidthChanged), + format!("Width: {}px", self.width) + ), + with_value( + slider(0.0..=1.0, self.opacity, Message::OpacityChanged) + .step(0.01), + format!("Opacity: {:.2}", self.opacity) + ), + with_value( + row![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + checkbox("Spin!", self.spin) + .text_size(12) + .on_toggle(Message::SpinToggled) + .size(12) + ] + .spacing(10) + .align_items(Alignment::Center), + format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) + ) + ] + .spacing(10) + .align_items(Alignment::End); + + container(column![fit, center(i_am_ferris), properties].spacing(10)) + .padding(10) + .into() + } +} + +impl Default for Image { + fn default() -> Self { + Self { + width: 300.0, + opacity: 1.0, + rotation: Rotation::default(), + content_fit: ContentFit::default(), + spin: false, + last_tick: Instant::now(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RotationStrategy { + Floating, + Solid, +} + +impl std::fmt::Display for RotationStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Floating => "Floating", + Self::Solid => "Solid", + }) + } +} + +fn with_value<'a>( + control: impl Into>, + value: String, +) -> Element<'a, Message> { + column![control.into(), text(value).size(12).line_height(1.0)] + .spacing(2) + .align_items(Alignment::Center) + .into() +} diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9cbb7fff..0716b2a4 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) @@ -619,9 +602,7 @@ mod grid { frame.into_geometry() }; - if self.scaling < 0.2 || !self.show_lines { - vec![life, overlay] - } else { + if self.scaling >= 0.2 && self.show_lines { let grid = self.grid_cache.draw(renderer, bounds.size(), |frame| { frame.translate(center); @@ -658,6 +639,8 @@ mod grid { }); vec![life, grid, overlay] + } else { + vec![life, overlay] } } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 1ccc4dd6..bf7801a9 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -6,7 +6,10 @@ mod rainbow { use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; use iced::mouse; - use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector}; + use iced::{ + Element, Length, Rectangle, Renderer, Size, Theme, Transformation, + Vector, + }; #[derive(Debug, Clone, Copy, Default)] pub struct Rainbow; @@ -44,7 +47,9 @@ mod rainbow { cursor: mouse::Cursor, _viewport: &Rectangle, ) { - use iced::advanced::graphics::mesh::{self, Mesh, SolidVertex2D}; + use iced::advanced::graphics::mesh::{ + self, Mesh, Renderer as _, SolidVertex2D, + }; use iced::advanced::Renderer as _; let bounds = layout.bounds(); @@ -77,7 +82,6 @@ mod rainbow { let posn_l = [0.0, bounds.height / 2.0]; let mesh = Mesh::Solid { - size: bounds.size(), buffers: mesh::Indexed { vertices: vec![ SolidVertex2D { @@ -128,6 +132,8 @@ mod rainbow { 0, 8, 1, // L ], }, + transformation: Transformation::IDENTITY, + clip_bounds: Rectangle::INFINITE, }; renderer.with_translation( @@ -147,51 +153,30 @@ mod rainbow { } use iced::widget::{column, container, scrollable}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::Element; 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); + ] + .padding(20) + .spacing(20) + .max_width(500); - let scrollable = - scrollable(container(content).width(Length::Fill).center_x()); + let scrollable = scrollable(container(content).center_x()); - container(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() - } + container(scrollable).center_y().into() } diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index a021c164..2b906c32 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, @@ -72,19 +60,15 @@ impl Sandbox for Gradient { } = *self; let gradient_box = container(horizontal_space()) - .width(Length::Fill) - .height(Length::Fill) - .style(move |_: &_| { + .style(move |_theme| { 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 996cdc17..bac0640c 100644 --- a/examples/integration/README.md +++ b/examples/integration/README.md @@ -10,25 +10,8 @@ The __[`main`]__ file contains all the code of the example. You can run it with `cargo run`: ``` -cargo run --package integration_wgpu +cargo run --package integration ``` -### How to run this example with WebGL backend -NOTE: Currently, WebGL backend is is still experimental, so expect bugs. - -```sh -# 0. Install prerequisites -cargo install wasm-bindgen-cli https -# 1. cd to the current folder -# 2. Compile wasm module -cargo build -p integration_wgpu --target wasm32-unknown-unknown -# 3. Invoke wasm-bindgen -wasm-bindgen ../../target/wasm32-unknown-unknown/debug/integration_wgpu.wasm --out-dir . --target web --no-typescript -# 4. run http server -http -# 5. Open 127.0.0.1:8000 in browser -``` - - [`main`]: src/main.rs [`wgpu`]: https://github.com/gfx-rs/wgpu diff --git a/examples/integration/index.html b/examples/integration/index.html deleted file mode 100644 index 920bc4a0..00000000 --- a/examples/integration/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Iced - wgpu + WebGL integration - - -

integration_wgpu

- - - - - diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab828..28050f8a 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 63efcbea..b8755aa6 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -5,315 +5,347 @@ use controls::Controls; use scene::Scene; use iced_wgpu::graphics::Viewport; -use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_wgpu::{wgpu, Engine, Renderer}; 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::style::Theme; use iced_winit::winit; use iced_winit::Clipboard; use winit::{ - event::{Event, WindowEvent}, + event::WindowEvent, event_loop::{ControlFlow, EventLoop}, keyboard::ModifiersState, }; use std::sync::Arc; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; -#[cfg(target_arch = "wasm32")] -use web_sys::HtmlCanvasElement; -#[cfg(target_arch = "wasm32")] -use winit::platform::web::WindowBuilderExtWebSys; - -pub fn main() -> Result<(), Box> { - #[cfg(target_arch = "wasm32")] - let canvas_element = { - console_log::init().expect("Initialize logger"); - - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - - web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.get_element_by_id("iced_canvas")) - .and_then(|element| element.dyn_into::().ok()) - .expect("Get canvas element") - }; - - #[cfg(not(target_arch = "wasm32"))] +pub fn main() -> Result<(), winit::error::EventLoopError> { tracing_subscriber::fmt::init(); // Initialize winit let event_loop = EventLoop::new()?; - #[cfg(target_arch = "wasm32")] - let window = winit::window::WindowBuilder::new() - .with_canvas(Some(canvas_element)) - .build(&event_loop)?; + #[allow(clippy::large_enum_variant)] + enum Runner { + Loading, + Ready { + window: Arc, + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + format: wgpu::TextureFormat, + engine: Engine, + renderer: Renderer, + scene: Scene, + state: program::State, + cursor_position: Option>, + clipboard: Clipboard, + viewport: Viewport, + modifiers: ModifiersState, + resized: bool, + }, + } - #[cfg(not(target_arch = "wasm32"))] - let window = winit::window::Window::new(&event_loop)?; + impl winit::application::ApplicationHandler for Runner { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + if let Self::Loading = self { + let window = Arc::new( + event_loop + .create_window( + winit::window::WindowAttributes::default(), + ) + .expect("Create window"), + ); - let window = Arc::new(window); + let physical_size = window.inner_size(); + let viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let clipboard = Clipboard::connect(&window); - let physical_size = window.inner_size(); - let mut viewport = Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - window.scale_factor(), - ); - let mut cursor_position = None; - let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(&window); + let backend = + wgpu::util::backend_bits_from_env().unwrap_or_default(); - // Initialize wgpu - #[cfg(target_arch = "wasm32")] - let default_backend = wgpu::Backends::GL; - #[cfg(not(target_arch = "wasm32"))] - let default_backend = wgpu::Backends::PRIMARY; + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: backend, + ..Default::default() + }); + let surface = instance + .create_surface(window.clone()) + .expect("Create window surface"); - let backend = - wgpu::util::backend_bits_from_env().unwrap_or(default_backend); + let (format, adapter, device, queue) = + futures::futures::executor::block_on(async { + let adapter = + wgpu::util::initialize_adapter_from_env_or_default( + &instance, + Some(&surface), + ) + .await + .expect("Create adapter"); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: backend, - ..Default::default() - }); - let surface = instance.create_surface(window.clone())?; + let adapter_features = adapter.features(); - let (format, adapter, device, queue) = - futures::futures::executor::block_on(async { - let adapter = wgpu::util::initialize_adapter_from_env_or_default( - &instance, - Some(&surface), - ) - .await - .expect("Create adapter"); + let capabilities = surface.get_capabilities(&adapter); - let adapter_features = adapter.features(); + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: adapter_features + & wgpu::Features::default(), + required_limits: wgpu::Limits::default(), + }, + None, + ) + .await + .expect("Request device"); - #[cfg(target_arch = "wasm32")] - let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); + ( + capabilities + .formats + .iter() + .copied() + .find(wgpu::TextureFormat::is_srgb) + .or_else(|| { + capabilities.formats.first().copied() + }) + .expect("Get preferred format"), + adapter, + device, + queue, + ) + }); - #[cfg(not(target_arch = "wasm32"))] - let needed_limits = wgpu::Limits::default(); - - 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, + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: physical_size.width, + height: physical_size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, }, - None, - ) - .await - .expect("Request device"); + ); - ( - capabilities - .formats - .iter() - .copied() - .find(wgpu::TextureFormat::is_srgb) - .or_else(|| capabilities.formats.first().copied()) - .expect("Get preferred format"), - adapter, + // Initialize scene and GUI controls + let scene = Scene::new(&device, format); + let controls = Controls::new(); + + // Initialize iced + let engine = + Engine::new(&adapter, &device, &queue, format, None); + let mut renderer = Renderer::new( + &device, + &engine, + Font::default(), + Pixels::from(16), + ); + + let state = program::State::new( + controls, + viewport.logical_size(), + &mut renderer, + ); + + // You should change this if you want to render continuously + event_loop.set_control_flow(ControlFlow::Wait); + + *self = Self::Ready { + window, + device, + queue, + surface, + format, + engine, + renderer, + scene, + state, + cursor_position: None, + modifiers: ModifiersState::default(), + clipboard, + viewport, + resized: false, + }; + } + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let Self::Ready { + window, device, queue, - ) - }); + surface, + format, + engine, + renderer, + scene, + state, + viewport, + cursor_position, + modifiers, + clipboard, + resized, + } = self + else { + return; + }; - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format, - width: physical_size.width, - height: physical_size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); + match event { + WindowEvent::RedrawRequested => { + if *resized { + let size = window.inner_size(); - let mut resized = false; - - // Initialize scene and GUI controls - let scene = Scene::new(&device, format); - let controls = Controls::new(); - - // Initialize iced - let mut renderer = Renderer::new( - Backend::new(&adapter, &device, &queue, Settings::default(), format), - Font::default(), - Pixels(16.0), - ); - - let mut state = - program::State::new(controls, viewport.logical_size(), &mut renderer); - - // Run event loop - event_loop.run(move |event, window_target| { - // You should change this if you want to render continuosly - window_target.set_control_flow(ControlFlow::Wait); - - match event { - Event::WindowEvent { - event: WindowEvent::RedrawRequested, - .. - } => { - if resized { - let size = window.inner_size(); - - viewport = Viewport::with_physical_size( - Size::new(size.width, size.height), - window.scale_factor(), - ); - - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); - - resized = false; - } - - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, + *viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor(), ); - let program = state.program(); - - let view = frame.texture.create_view( - &wgpu::TextureViewDescriptor::default(), + surface.configure( + device, + &wgpu::SurfaceConfiguration { + format: *format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }, ); - { - // We clear the frame - let mut render_pass = Scene::clear( - &view, - &mut encoder, - program.background_color(), + *resized = false; + } + + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, ); - // Draw the scene - scene.draw(&mut render_pass); - } + let program = state.program(); - // And then iced on top - renderer.with_primitives(|backend, primitive| { - backend.present( - &device, - &queue, + let view = frame.texture.create_view( + &wgpu::TextureViewDescriptor::default(), + ); + + { + // We clear the frame + let mut render_pass = Scene::clear( + &view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } + + // And then iced on top + renderer.present( + engine, + device, + queue, &mut encoder, None, frame.texture.format(), &view, - primitive, - &viewport, + viewport, ); - }); - // Then we submit the work - queue.submit(Some(encoder.finish())); - frame.present(); + // Then we submit the work + engine.submit(queue, encoder); + frame.present(); - // Update the mouse cursor - window.set_cursor_icon( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); - } - Err(error) => match error { - wgpu::SurfaceError::OutOfMemory => { - panic!( - "Swapchain error: {error}. \ + // Update the mouse cursor + window.set_cursor( + iced_winit::conversion::mouse_interaction( + state.mouse_interaction(), + ), + ); + } + Err(error) => match error { + wgpu::SurfaceError::OutOfMemory => { + panic!( + "Swapchain error: {error}. \ Rendering cannot continue." + ) + } + _ => { + // Try rendering again next frame. + window.request_redraw(); + } + }, + } + } + WindowEvent::CursorMoved { position, .. } => { + *cursor_position = Some(position); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + *modifiers = new_modifiers.state(); + } + WindowEvent::Resized(_) => { + *resized = true; + } + WindowEvent::CloseRequested => { + event_loop.exit(); + } + _ => {} + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, + event, + window.scale_factor(), + *modifiers, + ) { + state.queue_event(event); + } + + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + cursor_position + .map(|p| { + conversion::cursor_position( + p, + viewport.scale_factor(), ) - } - _ => { - // Try rendering again next frame. - window.request_redraw(); - } + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable), + renderer, + &Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, }, - } + clipboard, + ); + + // and request a redraw + window.request_redraw(); } - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = Some(position); - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers.state(); - } - WindowEvent::Resized(_) => { - resized = true; - } - WindowEvent::CloseRequested => { - window_target.exit(); - } - _ => {} - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, - event, - window.scale_factor(), - modifiers, - ) { - state.queue_event(event); - } - } - _ => {} } + } - // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( - viewport.logical_size(), - cursor_position - .map(|p| { - conversion::cursor_position(p, viewport.scale_factor()) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable), - &mut renderer, - &Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - &mut clipboard, - ); - - // and request a redraw - window.request_redraw(); - } - })?; - - Ok(()) + let mut runner = Runner::Loading; + event_loop.run_app(&mut runner) } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 06a476be..5f14c03b 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, + button, canvas, center, checkbox, column, container, horizontal_space, + pick_list, 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 { @@ -93,22 +76,18 @@ impl Application for Layout { .spacing(20) .align_items(Alignment::Center); - let example = container(if self.explain { + let example = center(if self.explain { self.example.view().explain(color!(0x0000ff)) } else { self.example.view() }) - .style(|theme: &Theme| { + .style(|theme| { let palette = theme.extended_palette(); - container::Appearance::default() + container::Style::default() .with_border(palette.background.strong.color, 4.0) }) - .padding(4) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(); + .padding(4); let controls = row([ (!self.example.is_first()).then_some( @@ -167,10 +146,6 @@ impl Example { title: "Application", view: application, }, - Self { - title: "Nested Quotes", - view: nested_quotes, - }, ]; fn is_first(self) -> bool { @@ -216,12 +191,7 @@ impl Default for Example { } fn centered<'a>() -> Element<'a, Message> { - container(text("I am centered!").size(50)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(text("I am centered!").size(50)).into() } fn column_<'a>() -> Element<'a, Message> { @@ -266,10 +236,10 @@ fn application<'a>() -> Element<'a, Message> { .padding(10) .align_items(Alignment::Center), ) - .style(|theme: &Theme| { + .style(|theme| { let palette = theme.extended_palette(); - container::Appearance::default() + container::Style::default() .with_border(palette.background.strong.color, 1) }); @@ -280,8 +250,7 @@ fn application<'a>() -> Element<'a, Message> { .width(200) .align_items(Alignment::Center), ) - .style(theme::Container::Box) - .height(Length::Fill) + .style(container::rounded_box) .center_y(); let content = container( @@ -304,38 +273,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 9d8c0e35..c3f6b8de 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,11 +170,10 @@ 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())), + text(item.name.clone()).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/circular.rs b/examples/loading_spinners/src/circular.rs index 12670ed1..de728af2 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -358,7 +358,7 @@ where |renderer| { use iced::advanced::graphics::geometry::Renderer as _; - renderer.draw(vec![geometry]); + renderer.draw_geometry(geometry); }, ); } diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 93a4605e..e8d67ab5 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::widget::{center, column, row, slider, text}; +use iced::Element; 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 { @@ -94,7 +73,7 @@ impl Application for LoadingSpinners { }) .spacing(20); - container( + center( column.push( row![ text("Cycle duration:"), @@ -108,10 +87,14 @@ impl Application for LoadingSpinners { .spacing(20.0), ), ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .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 8602edb7..c4d3b449 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -1,59 +1,46 @@ -use iced::widget::{button, column, container, text}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::widget::{button, center, column, text}; +use iced::{Alignment, Element}; 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; } } } fn view(&self) -> Element { - container(loupe( + center(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), )) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } } @@ -168,7 +155,7 @@ mod loupe { if cursor.is_over(layout.bounds()) { mouse::Interaction::ZoomIn } else { - mouse::Interaction::Idle + mouse::Interaction::None } } } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 938ce32c..a012c310 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -1,21 +1,18 @@ 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, + self, button, center, column, container, horizontal_space, mouse_area, + opaque, pick_list, row, stack, text, text_input, }; +use iced::{Alignment, Color, 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 +34,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) } @@ -114,13 +98,7 @@ impl Application for App { 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) - ) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill), + center(button(text("Show Modal")).on_press(Message::ShowModal)), row![ text("Bottom Left"), horizontal_space(), @@ -131,12 +109,10 @@ impl Application for App { ] .height(Length::Fill), ) - .padding(10) - .width(Length::Fill) - .height(Length::Fill); + .padding(10); if self.show_modal { - let modal = container( + let signup = container( column![ text("Sign Up").size(24), column![ @@ -175,11 +151,9 @@ impl Application for App { ) .width(300) .padding(10) - .style(theme::Container::Box); + .style(container::rounded_box); - Modal::new(content, modal) - .on_blur(Message::HideModal) - .into() + modal(content, signup, Message::HideModal) } else { content.into() } @@ -218,326 +192,29 @@ impl fmt::Display for Plan { } } -mod modal { - use iced::advanced::layout::{self, Layout}; - use iced::advanced::overlay; - use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; - use iced::advanced::{self, Clipboard, Shell}; - use iced::alignment::Alignment; - use iced::event; - use iced::mouse; - use iced::{Color, Element, Event, Length, Point, Rectangle, Size, Vector}; - - /// A widget that centers a modal element over some base element - pub struct Modal<'a, Message, Theme, Renderer> { - base: Element<'a, Message, Theme, Renderer>, - modal: Element<'a, Message, Theme, Renderer>, - on_blur: Option, - } - - impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> { - /// Returns a new [`Modal`] - pub fn new( - base: impl Into>, - modal: impl Into>, - ) -> Self { - Self { - base: base.into(), - modal: modal.into(), - on_blur: None, - } - } - - /// Sets the message that will be produces when the background - /// of the [`Modal`] is pressed - pub fn on_blur(self, on_blur: Message) -> Self { - Self { - on_blur: Some(on_blur), - ..self - } - } - } - - impl<'a, Message, Theme, Renderer> Widget - for Modal<'a, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn children(&self) -> Vec { - vec![ - widget::Tree::new(&self.base), - widget::Tree::new(&self.modal), - ] - } - - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); - } - - fn size(&self) -> Size { - self.base.as_widget().size() - } - - fn layout( - &self, - tree: &mut widget::Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.base.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - } - - fn on_event( - &mut self, - state: &mut widget::Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - self.base.as_widget_mut().on_event( - &mut state.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } - - fn draw( - &self, - state: &widget::Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.base.as_widget().draw( - &state.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut widget::Tree, - layout: Layout<'_>, - _renderer: &Renderer, - translation: Vector, - ) -> Option> { - Some(overlay::Element::new(Box::new(Overlay { - position: layout.position() + translation, - content: &mut self.modal, - tree: &mut state.children[1], - size: layout.bounds().size(), - on_blur: self.on_blur.clone(), - }))) - } - - fn mouse_interaction( - &self, - state: &widget::Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.base.as_widget().mouse_interaction( - &state.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn operate( - &self, - state: &mut widget::Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.base.as_widget().operate( - &mut state.children[0], - layout, - renderer, - operation, - ); - } - } - - struct Overlay<'a, 'b, Message, Theme, Renderer> { - position: Point, - content: &'b mut Element<'a, Message, Theme, Renderer>, - tree: &'b mut widget::Tree, - size: Size, - on_blur: Option, - } - - impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay - for Overlay<'a, 'b, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn layout( - &mut self, - renderer: &Renderer, - _bounds: Size, - ) -> layout::Node { - let limits = layout::Limits::new(Size::ZERO, self.size) - .width(Length::Fill) - .height(Length::Fill); - - let child = self - .content - .as_widget() - .layout(self.tree, renderer, &limits) - .align(Alignment::Center, Alignment::Center, limits.max()); - - layout::Node::with_children(self.size, vec![child]) - .move_to(self.position) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let content_bounds = layout.children().next().unwrap().bounds(); - - if let Some(message) = self.on_blur.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) = &event - { - if !cursor.is_over(content_bounds) { - shell.publish(message.clone()); - return event::Status::Captured; +fn modal<'a, Message>( + base: impl Into>, + content: impl Into>, + on_blur: Message, +) -> Element<'a, Message> +where + Message: Clone + 'a, +{ + stack![ + base.into(), + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK } - } + .into(), + ), + ..container::Style::default() } - - self.content.as_widget_mut().on_event( - self.tree, - event, - layout.children().next().unwrap(), - cursor, - renderer, - clipboard, - shell, - &layout.bounds(), - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - ..renderer::Quad::default() - }, - Color { - a: 0.80, - ..Color::BLACK - }, - ); - - self.content.as_widget().draw( - self.tree, - renderer, - theme, - style, - layout.children().next().unwrap(), - cursor, - &layout.bounds(), - ); - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.content.as_widget().operate( - self.tree, - layout.children().next().unwrap(), - renderer, - operation, - ); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - self.tree, - layout.children().next().unwrap(), - cursor, - viewport, - renderer, - ) - } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - self.tree, - layout.children().next().unwrap(), - renderer, - Vector::ZERO, - ) - } - } - - impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> - where - Theme: 'a, - Message: 'a + Clone, - Renderer: 'a + advanced::Renderer, - { - fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self { - Element::new(modal) - } - } + })) + .on_press(on_blur) + ] + .into() } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 5a5e70c1..74339e0c 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,7 +1,9 @@ use iced::event; use iced::executor; use iced::multi_window::{self, Application}; -use iced::widget::{button, column, container, scrollable, text, text_input}; +use iced::widget::{ + button, center, column, container, scrollable, text, text_input, +}; use iced::window; use iced::{ Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, @@ -128,12 +130,7 @@ impl multi_window::Application for Example { fn view(&self, window: window::Id) -> Element { let content = self.windows.get(&window).unwrap().view(window); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } fn theme(&self, window: window::Id) -> Self::Theme { @@ -210,6 +207,6 @@ impl Window { .align_items(Alignment::Center), ); - container(content).width(200).center_x().into() + container(content).center_x().width(200).into() } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 956ad471..2453c7f5 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 39719420..e74ea1ee 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(), - ) + Example { + panes, + panes_created: 1, + focus: None, + } } - fn title(&self) -> String { - String::from("Pane grid - Iced") - } - - 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, @@ -287,10 +277,7 @@ fn view_content<'a>( ) ] .push_maybe(if total_panes > 1 && !is_pinned { - Some( - button("Close", Message::Close(pane)) - .style(theme::Button::Destructive), - ) + Some(button("Close", Message::Close(pane)).style(button::danger)) } else { None }) @@ -304,12 +291,7 @@ fn view_content<'a>( .spacing(10) .align_items(Alignment::Center); - container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() + container(scrollable(content)).center_y().padding(5).into() } fn view_controls<'a>( @@ -327,7 +309,7 @@ fn view_controls<'a>( Some( button(text(content).size(14)) - .style(theme::Button::Secondary) + .style(button::secondary) .padding(3) .on_press(message), ) @@ -336,7 +318,7 @@ fn view_controls<'a>( }); let close = button(text("Close").size(14)) - .style(theme::Button::Destructive) + .style(button::danger) .padding(3) .on_press_maybe(if total_panes > 1 && !is_pinned { Some(Message::Close(pane)) @@ -351,30 +333,30 @@ 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) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { text_color: Some(palette.background.strong.text), background: Some(palette.background.strong.color.into()), ..Default::default() } } - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + pub fn title_bar_focused(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { text_color: Some(palette.primary.strong.text), background: Some(palette.primary.strong.color.into()), ..Default::default() } } - pub fn pane_active(theme: &Theme) -> container::Appearance { + pub fn pane_active(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { background: Some(palette.background.weak.color.into()), border: Border { width: 2.0, @@ -385,10 +367,10 @@ mod style { } } - pub fn pane_focused(theme: &Theme) -> container::Appearance { + pub fn pane_focused(theme: &Theme) -> container::Style { let palette = theme.extended_palette(); - container::Appearance { + container::Style { background: Some(palette.background.weak.color.into()), border: Border { width: 2.0, diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index c40493e2..2be6f5b0 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) => { diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8b71a269..be20094d 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::widget::{self, center, column, image, row, text}; +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() } }, } @@ -88,12 +83,7 @@ impl Application for Pokedex { .align_items(Alignment::End), }; - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } } @@ -116,7 +106,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), @@ -193,7 +183,7 @@ impl Pokemon { { let bytes = reqwest::get(&url).await?.bytes().await?; - Ok(image::Handle::from_memory(bytes)) + Ok(image::Handle::from_bytes(bytes)) } #[cfg(target_arch = "wasm32")] diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index d4ebe4d3..67da62f2 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 36f79a31..c6a90458 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -1,10 +1,14 @@ -use iced::widget::{ - column, container, pick_list, qr_code, row, text, text_input, -}; -use iced::{Alignment, Element, Length, Sandbox, Settings, Theme}; +use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; +use iced::{Alignment, Element, 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)] @@ -20,17 +24,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) => { @@ -76,13 +70,7 @@ impl Sandbox for QRGenerator { .spacing(20) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .center_x() - .center_y() - .into() + center(content).padding(20).into() } fn theme(&self) -> Theme { diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 79749956..82495a1a 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,10 +106,10 @@ 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( + image(image::Handle::from_rgba( screenshot.size.width, screenshot.size.height, screenshot.clone(), @@ -148,12 +123,10 @@ impl Application for Example { }; let image = container(image) + .center_y() .padding(10) - .style(theme::Container::Box) - .width(Length::FillPortion(2)) - .height(Length::Fill) - .center_x() - .center_y(); + .style(container::rounded_box) + .width(Length::FillPortion(2)); let crop_origin_controls = row![ text("X:") @@ -216,9 +189,9 @@ impl Application for Example { ) } else { button(centered_text("Saving...")) - .style(theme::Button::Secondary) + .style(button::secondary) } - .style(theme::Button::Secondary) + .style(button::secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) ] @@ -227,7 +200,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(theme::Button::Destructive) + .style(button::danger) .padding([10, 20, 10, 20]) .width(Length::Fill), ] @@ -238,12 +211,7 @@ impl Application for Example { .spacing(40) }; - let side_content = container(controls) - .align_x(alignment::Horizontal::Center) - .width(Length::FillPortion(1)) - .height(Length::Fill) - .center_y() - .center_x(); + let side_content = container(controls).center_y(); let content = row![side_content, image] .spacing(10) @@ -251,16 +219,10 @@ impl Application for Example { .height(Length::Fill) .align_items(Alignment::Center); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .center_x() - .center_y() - .into() + container(content).padding(10).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 bae23775..bbb6497f 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,19 +1,22 @@ -use iced::executor; use iced::widget::scrollable::Properties; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, Scrollable, }; -use iced::{ - Alignment, Application, Color, Command, Element, Length, Settings, Theme, -}; +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 { @@ -44,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 { @@ -336,18 +327,24 @@ impl Application for ScrollableDemo { .spacing(10) .into(); - container(content).padding(20).center_x().center_y().into() + container(content).padding(20).into() } - fn theme(&self) -> Self::Theme { + fn theme(&self) -> Theme { Theme::Dark } } -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(), +impl Default for ScrollableDemo { + fn default() -> Self { + Self::new() + } +} + +fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Style { + progress_bar::Style { + background: theme.extended_palette().background.strong.color.into(), + bar: Color::from_rgb8(250, 85, 134).into(), + border: Border::default(), } } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 01a114bb..9cd6237f 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::canvas::{self, Canvas, Geometry}; 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) @@ -134,7 +111,7 @@ impl canvas::Program for SierpinskiGraph { _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec { + ) -> Vec { let geom = self.cache.draw(renderer, bounds.size(), |frame| { frame.stroke( &canvas::Path::rectangle(Point::ORIGIN, frame.size()), @@ -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 f71dac01..5ffdc9c6 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::widget::{center, column, container, slider, text, vertical_slider}; +use iced::Element; 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) => { @@ -60,18 +54,20 @@ impl Sandbox for Slider { let text = text(self.value); - container( + center( column![ - container(v_slider).width(Length::Fill).center_x(), - container(h_slider).width(Length::Fill).center_x(), - container(text).width(Length::Fill).center_x(), + container(v_slider).center_x(), + container(h_slider).center_x(), + container(text).center_x() ] .spacing(25), ) - .height(Length::Fill) - .width(Length::Fill) - .center_x() - .center_y() .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 a58ca683..deb211d8 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::widget::canvas::{Geometry, 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 { @@ -159,7 +130,7 @@ impl canvas::Program for State { _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, - ) -> Vec { + ) -> Vec { use std::f32::consts::PI; let background = @@ -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 8a0674c1..bbe9d0ff 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::widget::{button, center, column, row, text}; +use iced::{Alignment, Element, 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); @@ -145,12 +128,7 @@ impl Application for Stopwatch { .align_items(Alignment::Center) .spacing(20); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } fn theme(&self) -> Theme { diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index befdfc1b..57e8f47e 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -1,12 +1,14 @@ use iced::widget::{ - button, checkbox, column, container, horizontal_rule, pick_list, - progress_bar, row, scrollable, slider, text, text_input, toggler, - vertical_rule, vertical_space, + button, center, checkbox, column, horizontal_rule, pick_list, 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) => { @@ -114,12 +106,7 @@ impl Sandbox for Styling { .padding(20) .max_width(600); - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content).into() } fn theme(&self) -> Theme { diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index ba93007c..45b46716 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::widget::{center, checkbox, column, container, svg}; +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::Style { + color: if self.apply_color_filter { + Some(color!(0x0000ff)) + } else { + None + }, }, ); @@ -55,19 +44,12 @@ impl Sandbox for Tiger { checkbox("Apply a color filter", self.apply_color_filter) .on_toggle(Message::ToggleColorFilter); - container( - column![ - svg, - container(apply_color_filter).width(Length::Fill).center_x() - ] - .spacing(20) - .height(Length::Fill), + center( + column![svg, container(apply_color_filter).center_x()] + .spacing(20) + .height(Length::Fill), ) - .width(Length::Fill) - .height(Length::Fill) .padding(20) - .center_x() - .center_y() .into() } } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 31dc92f1..89a8383a 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}; 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 } => { @@ -149,11 +136,6 @@ impl Application for Example { } }; - container(content) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) - .into() + container(content).center().into() } } diff --git a/examples/the_matrix/Cargo.toml b/examples/the_matrix/Cargo.toml new file mode 100644 index 00000000..775e76e0 --- /dev/null +++ b/examples/the_matrix/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "the_matrix" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["canvas", "tokio", "debug"] + +rand = "0.8" +tracing-subscriber = "0.3" diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs new file mode 100644 index 00000000..f3a67ac8 --- /dev/null +++ b/examples/the_matrix/src/main.rs @@ -0,0 +1,115 @@ +use iced::mouse; +use iced::time::{self, Instant}; +use iced::widget::canvas; +use iced::{ + Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription, + Theme, +}; + +use std::cell::RefCell; + +pub fn main() -> iced::Result { + tracing_subscriber::fmt::init(); + + iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) + .subscription(TheMatrix::subscription) + .antialiasing(true) + .run() +} + +#[derive(Default)] +struct TheMatrix { + tick: usize, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Tick(Instant), +} + +impl TheMatrix { + fn update(&mut self, message: Message) { + match message { + Message::Tick(_now) => { + self.tick += 1; + } + } + } + + fn view(&self) -> Element { + canvas(self as &Self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn subscription(&self) -> Subscription { + time::every(std::time::Duration::from_millis(50)).map(Message::Tick) + } +} + +impl canvas::Program for TheMatrix { + type State = RefCell>; + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + use rand::distributions::Distribution; + use rand::Rng; + + const CELL_SIZE: f32 = 10.0; + + let mut caches = state.borrow_mut(); + + if caches.is_empty() { + let group = canvas::Group::unique(); + + caches.resize_with(30, || canvas::Cache::with_group(group)); + } + + vec![caches[self.tick % caches.len()].draw( + renderer, + bounds.size(), + |frame| { + frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK); + + let mut rng = rand::thread_rng(); + let rows = (frame.height() / CELL_SIZE).ceil() as usize; + let columns = (frame.width() / CELL_SIZE).ceil() as usize; + + for row in 0..rows { + for column in 0..columns { + let position = Point::new( + column as f32 * CELL_SIZE, + row as f32 * CELL_SIZE, + ); + + let alphas = [0.05, 0.1, 0.2, 0.5]; + let weights = [10, 4, 2, 1]; + let distribution = + rand::distributions::WeightedIndex::new(weights) + .expect("Create distribution"); + + frame.fill_text(canvas::Text { + content: rng.gen_range('!'..'z').to_string(), + position, + color: Color { + a: alphas[distribution.sample(&mut rng)], + g: 1.0, + ..Color::BLACK + }, + size: CELL_SIZE.into(), + font: Font::MONOSPACE, + ..canvas::Text::default() + }); + } + } + }, + )] + } +} diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index c1d29193..0fcf08c4 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, + self, button, center, column, pick_list, row, slider, text, text_input, }; +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(), - ) +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 title(&self) -> String { - String::from("Toast - Iced") - } - - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { event::listen().map(Message::Event) } @@ -106,8 +92,8 @@ 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) }; @@ -116,7 +102,7 @@ impl Application for App { .then_some(Message::Add), ); - let content = container( + let content = center( column![ subtitle( "Title", @@ -160,11 +146,7 @@ impl Application for App { ] .spacing(10) .max_width(200), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(); + ); toast::Manager::new(content, &self.toasts, Message::Close) .timeout(self.timeout_secs) @@ -172,6 +154,12 @@ impl Application for App { } } +impl Default for App { + fn default() -> Self { + Self::new() + } +} + mod toast { use std::fmt; use std::time::{Duration, Instant}; @@ -209,27 +197,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 { @@ -282,14 +249,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() @@ -676,4 +646,36 @@ mod toast { Element::new(manager) } } + + fn styled(pair: theme::palette::Pair) -> container::Style { + container::Style { + background: Some(pair.color.into()), + text_color: pair.text.into(), + ..Default::default() + } + } + + fn primary(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + styled(palette.primary.weak) + } + + fn secondary(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + styled(palette.secondary.weak) + } + + fn success(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + styled(palette.success.weak) + } + + fn danger(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + styled(palette.danger.weak) + } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index eae127f7..8119bc91 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, + self, button, center, 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) @@ -254,7 +238,7 @@ impl Application for Todos { .spacing(20) .max_width(800); - scrollable(container(content).padding(40).center_x()).into() + scrollable(container(content).center_x().padding(40)).into() } } } @@ -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) @@ -450,27 +435,23 @@ impl Filter { } fn loading_message<'a>() -> Element<'a, Message> { - container( + center( text("Loading...") .horizontal_alignment(alignment::Horizontal::Center) .size(50), ) - .width(Length::Fill) - .height(Length::Fill) - .center_y() .into() } fn empty_message(message: &str) -> Element<'_, Message> { - container( + center( text(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() .into() } diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index a904cce0..f48f688a 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::widget::{button, center, container, tooltip}; +use iced::Element; 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,14 +41,9 @@ impl Sandbox for Example { self.position, ) .gap(10) - .style(theme::Container::Box); + .style(container::rounded_box); - container(tooltip) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(tooltip).into() } } diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 1e2f1ef8..bae6490d 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,18 +49,17 @@ impl Sandbox for Tour { fn view(&self) -> Element { let Tour { steps, .. } = self; - let controls = row![] - .push_maybe(steps.has_previous().then(|| { - button("Back") - .on_press(Message::BackPressed) - .style(theme::Button::Secondary) - })) - .push(horizontal_space()) - .push_maybe( - steps - .can_continue() - .then(|| 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), @@ -84,11 +76,10 @@ impl Sandbox for Tour { } else { content }) - .width(Length::Fill) .center_x(), ); - container(scrollable).height(Length::Fill).center_y().into() + container(scrollable).center_y().into() } } @@ -173,6 +164,12 @@ impl Steps { } } +impl Default for Steps { + fn default() -> Self { + Steps::new() + } +} + enum Step { Welcome, Slider { @@ -359,7 +356,7 @@ impl<'a> Step { .into() } - fn container(title: &str) -> Column<'a, StepMessage> { + fn container(title: &str) -> Column<'_, StepMessage> { column![text(title).size(50)].spacing(20) } @@ -474,7 +471,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) @@ -591,7 +588,7 @@ impl<'a> Step { value: &str, is_secure: bool, is_showing_icon: bool, - ) -> Column<'a, StepMessage> { + ) -> Column<'_, StepMessage> { let mut text_input = text_input("Type something to continue...", value) .on_input(StepMessage::InputChanged) .padding(10) @@ -672,12 +669,11 @@ fn ferris<'a>( .filter_method(filter_method) .width(width), ) - .width(Length::Fill) .center_x() } -fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button(text(label)).padding([12, 24]) +fn padded_button(label: &str) -> Button<'_, 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 bf570123..800a188b 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::widget::{center, text}; +use iced::{Element, 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 { @@ -60,11 +44,6 @@ impl Application for App { None => text("No URL received yet!"), }; - container(content.size(48)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + center(content.size(48)).into() } } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 0b9ea938..a7391e23 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) => { @@ -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 bef5d296..332b6a7b 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) => { @@ -82,7 +64,10 @@ impl Application for Example { row![ text(label), horizontal_space(), - text(value).font(Font::MONOSPACE).size(14).style(color), + 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,7 +104,7 @@ 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), @@ -131,7 +115,7 @@ impl Application for Example { container(text("I am the outer container!")) .id(OUTER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), + .style(container::rounded_box), vertical_space().height(400), scrollable( column![ @@ -140,7 +124,7 @@ impl Application for Example { container(text("I am the inner container!")) .id(INNER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), + .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/echo.rs b/examples/websocket/src/echo.rs index 281ed4bd..cd32cb66 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -2,6 +2,7 @@ pub mod server; use iced::futures; use iced::subscription::{self, Subscription}; +use iced::widget::text; use futures::channel::mpsc; use futures::sink::SinkExt; @@ -136,16 +137,24 @@ impl Message { pub fn disconnected() -> Self { Message::Disconnected } + + pub fn as_str(&self) -> &str { + match self { + Message::Connected => "Connected successfully!", + Message::Disconnected => "Connection lost... Retrying...", + Message::User(message) => message.as_str(), + } + } } impl fmt::Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Message::Connected => write!(f, "Connected successfully!"), - Message::Disconnected => { - write!(f, "Connection lost... Retrying...") - } - Message::User(message) => write!(f, "{message}"), - } + f.write_str(self.as_str()) + } +} + +impl<'a> text::IntoFragment<'a> for &'a Message { + fn into_fragment(self) -> text::Fragment<'a> { + text::Fragment::Borrowed(self.as_str()) } } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 38a6db1e..ba1e1029 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, + self, button, center, column, row, scrollable, text, text_input, }; +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,12 @@ 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(), +impl WebSocket { + fn load() -> Command { + Command::batch([ Command::perform(echo::server::run(), |_| Message::Server), - ) - } - - fn title(&self) -> String { - String::from("WebSocket - Iced") + widget::focus_next(), + ]) } fn update(&mut self, message: Message) -> Command { @@ -97,21 +88,15 @@ impl Application for WebSocket { fn view(&self) -> Element { let message_log: Element<_> = if self.messages.is_empty() { - container( + center( text("Your messages will appear here...") - .style(Color::from_rgb8(0x88, 0x88, 0x88)), + .color(color!(0x888888)), ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } else { scrollable( - column( - self.messages.iter().cloned().map(text).map(Element::from), - ) - .spacing(10), + column(self.messages.iter().map(text).map(Element::from)) + .spacing(10), ) .id(MESSAGE_LOG.clone()) .height(Length::Fill) diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 69a915e4..a6fcfde1 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -22,6 +25,7 @@ iced_core.workspace = true futures.workspace = true log.workspace = true +rustc-hash.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std.workspace = true diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs index 52b0e914..b7da5e90 100644 --- a/futures/src/backend/native/async_std.rs +++ b/futures/src/backend/native/async_std.rs @@ -18,8 +18,7 @@ impl crate::Executor for Executor { pub mod time { //! Listen and react to time. - use crate::core::Hasher; - use crate::subscription::{self, Subscription}; + use crate::subscription::{self, Hasher, Subscription}; /// Returns a [`Subscription`] that produces messages at a set interval. /// diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs index 00d13d35..aaf1518c 100644 --- a/futures/src/backend/native/smol.rs +++ b/futures/src/backend/native/smol.rs @@ -17,8 +17,7 @@ impl crate::Executor for Executor { pub mod time { //! Listen and react to time. - use crate::core::Hasher; - use crate::subscription::{self, Subscription}; + use crate::subscription::{self, Hasher, Subscription}; /// Returns a [`Subscription`] that produces messages at a set interval. /// diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs index 4698a105..df91d798 100644 --- a/futures/src/backend/native/tokio.rs +++ b/futures/src/backend/native/tokio.rs @@ -22,8 +22,7 @@ impl crate::Executor for Executor { pub mod time { //! Listen and react to time. - use crate::core::Hasher; - use crate::subscription::{self, Subscription}; + use crate::subscription::{self, Hasher, Subscription}; /// Returns a [`Subscription`] that produces messages at a set interval. /// @@ -56,13 +55,15 @@ pub mod time { let start = tokio::time::Instant::now() + self.0; + let mut interval = tokio::time::interval_at(start, self.0); + interval.set_missed_tick_behavior( + tokio::time::MissedTickBehavior::Skip, + ); + let stream = { - futures::stream::unfold( - tokio::time::interval_at(start, self.0), - |mut interval| async move { - Some((interval.tick().await, interval)) - }, - ) + futures::stream::unfold(interval, |mut interval| async move { + Some((interval.tick().await, interval)) + }) }; stream.map(tokio::time::Instant::into_std).boxed() diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs index 2666f1b4..3228dd18 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)] @@ -16,8 +16,7 @@ impl crate::Executor for Executor { pub mod time { //! Listen and react to time. - use crate::core::Hasher; - use crate::subscription::{self, Subscription}; + use crate::subscription::{self, Hasher, Subscription}; use crate::BoxStream; /// Returns a [`Subscription`] that produces messages at a set interval. diff --git a/futures/src/lib.rs b/futures/src/lib.rs index b0acb76f..a874a618 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -4,13 +4,6 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use futures; pub use iced_core as core; diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 7537c022..93e35608 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -4,7 +4,6 @@ mod tracker; pub use tracker::Tracker; use crate::core::event::{self, Event}; -use crate::core::Hasher; use crate::futures::{Future, Stream}; use crate::{BoxStream, MaybeSend}; @@ -18,6 +17,9 @@ use std::hash::Hash; /// It is the input of a [`Subscription`]. pub type EventStream = BoxStream<(Event, event::Status)>; +/// The hasher used for identifying subscriptions. +pub type Hasher = rustc_hash::FxHasher; + /// A request to listen to external events. /// /// Besides performing async actions on demand with `Command`, most diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 15ed5b87..277a446b 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,12 +1,11 @@ use crate::core::event::{self, Event}; -use crate::core::Hasher; -use crate::subscription::Recipe; +use crate::subscription::{Hasher, Recipe}; use crate::{BoxFuture, MaybeSend}; use futures::channel::mpsc; use futures::sink::{Sink, SinkExt}; +use rustc_hash::FxHashMap; -use std::collections::HashMap; use std::hash::Hasher as _; /// A registry of subscription streams. @@ -18,7 +17,7 @@ use std::hash::Hasher as _; /// [`Subscription`]: crate::Subscription #[derive(Debug, Default)] pub struct Tracker { - subscriptions: HashMap, + subscriptions: FxHashMap, } #[derive(Debug)] @@ -31,7 +30,7 @@ impl Tracker { /// Creates a new empty [`Tracker`]. pub fn new() -> Self { Self { - subscriptions: HashMap::new(), + subscriptions: FxHashMap::default(), } } diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 0ee6ff47..e8d27d07 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -34,7 +37,6 @@ raw-window-handle.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true -xxhash-rust.workspace = true image.workspace = true image.optional = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs deleted file mode 100644 index 10eb337f..00000000 --- a/graphics/src/backend.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Write a graphics backend. -use crate::core::image; -use crate::core::svg; -use crate::core::Size; - -use std::borrow::Cow; - -/// The graphics backend of a [`Renderer`]. -/// -/// [`Renderer`]: crate::Renderer -pub trait Backend { - /// The custom kind of primitives this [`Backend`] supports. - type Primitive; -} - -/// A graphics backend that supports text rendering. -pub trait Text { - /// Loads a font from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); -} - -/// A graphics backend that supports image rendering. -pub trait Image { - /// Returns the dimensions of the provided image. - fn dimensions(&self, handle: &image::Handle) -> Size; -} - -/// A graphics backend that supports SVG rendering. -pub trait Svg { - /// Returns the viewport dimensions of the provided SVG. - fn viewport_dimensions(&self, handle: &svg::Handle) -> Size; -} diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs new file mode 100644 index 00000000..bbba79eb --- /dev/null +++ b/graphics/src/cache.rs @@ -0,0 +1,189 @@ +//! Cache computations and efficiently reuse them. +use std::cell::RefCell; +use std::fmt; +use std::sync::atomic::{self, AtomicU64}; + +/// A simple cache that stores generated values to avoid recomputation. +/// +/// Keeps track of the last generated value after clearing. +pub struct Cache { + group: Group, + state: RefCell>, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + group: Group::singleton(), + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + assert!( + !group.is_singleton(), + "The group {group:?} cannot be shared!" + ); + + Cache { + group, + state: RefCell::new(State::Empty { previous: None }), + } + } + + /// Returns the [`Group`] of the [`Cache`]. + pub fn group(&self) -> Group { + self.group + } + + /// Puts the given value in the [`Cache`]. + /// + /// Notice that, given this is a cache, a mutable reference is not + /// necessary to call this method. You can safely update the cache in + /// rendering code. + pub fn put(&self, value: T) { + *self.state.borrow_mut() = State::Filled { current: value }; + } + + /// Returns a reference cell to the internal [`State`] of the [`Cache`]. + pub fn state(&self) -> &RefCell> { + &self.state + } + + /// Clears the [`Cache`]. + pub fn clear(&self) + where + T: Clone, + { + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { current } => Some(current.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; + } +} + +/// A cache group. +/// +/// Caches that share the same group generally change together. +/// +/// A cache group can be used to implement certain performance +/// optimizations during rendering, like batching or sharing atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Group { + id: u64, + is_singleton: bool, +} + +impl Group { + /// Generates a new unique cache [`Group`]. + pub fn unique() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + + Self { + id: NEXT.fetch_add(1, atomic::Ordering::Relaxed), + is_singleton: false, + } + } + + /// Returns `true` if the [`Group`] can only ever have a + /// single [`Cache`] in it. + /// + /// This is the default kind of [`Group`] assigned when using + /// [`Cache::new`]. + /// + /// Knowing that a [`Group`] will never be shared may be + /// useful for rendering backends to perform additional + /// optimizations. + pub fn is_singleton(self) -> bool { + self.is_singleton + } + + fn singleton() -> Self { + Self { + is_singleton: true, + ..Self::unique() + } + } +} + +impl fmt::Debug for Cache +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::ops::Deref; + + let state = self.state.borrow(); + + match state.deref() { + State::Empty { previous } => { + write!(f, "Cache::Empty {{ previous: {previous:?} }}") + } + State::Filled { current } => { + write!(f, "Cache::Filled {{ current: {current:?} }}") + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +/// The state of a [`Cache`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum State { + /// The [`Cache`] is empty. + Empty { + /// The previous value of the [`Cache`]. + previous: Option, + }, + /// The [`Cache`] is filled. + Filled { + /// The current value of the [`Cache`] + current: T, + }, +} + +/// A piece of data that can be cached. +pub trait Cached: Sized { + /// The type of cache produced. + type Cache: Clone; + + /// Loads the [`Cache`] into a proper instance. + /// + /// [`Cache`]: Self::Cache + fn load(cache: &Self::Cache) -> Self; + + /// Caches this value, producing its corresponding [`Cache`]. + /// + /// [`Cache`]: Self::Cache + fn cache(self, group: Group, previous: Option) -> Self::Cache; +} + +#[cfg(debug_assertions)] +impl Cached for () { + type Cache = (); + + fn load(_cache: &Self::Cache) -> Self {} + + fn cache( + self, + _group: Group, + _previous: Option, + ) -> Self::Cache { + } +} diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 786fd8a4..e2bfcbed 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -1,29 +1,40 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::{Error, Viewport}; - use crate::core::Color; use crate::futures::{MaybeSend, MaybeSync}; +use crate::{Error, Settings, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; +use std::borrow::Cow; +use std::future::Future; + /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { - /// The settings of the backend. - type Settings: Default; - /// The iced renderer of the backend. - type Renderer: iced_core::Renderer; + type Renderer; /// The surface of the backend. type Surface; /// Creates a new [`Compositor`]. fn new( - settings: Self::Settings, + settings: Settings, compatible_window: W, - ) -> Result; + ) -> impl Future> { + Self::with_backend(settings, compatible_window, None) + } + + /// Creates a new [`Compositor`] with a backend preference. + /// + /// If the backend does not match the preference, it will return + /// [`Error::GraphicsAdapterNotFound`]. + fn with_backend( + _settings: Settings, + _compatible_window: W, + _backend: Option<&str>, + ) -> impl Future>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. fn create_renderer(&self) -> Self::Renderer; @@ -51,6 +62,14 @@ pub trait Compositor: Sized { /// Returns [`Information`] used by this [`Compositor`]. fn fetch_information(&self) -> Information; + /// Loads a font from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>) { + crate::text::font_system() + .write() + .expect("Write to font system") + .load_font(font); + } + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. /// /// [`Renderer`]: Self::Renderer @@ -90,6 +109,12 @@ impl Window for T where { } +/// Defines the default compositor of a renderer. +pub trait Default { + /// The compositor of the renderer. + type Compositor: Compositor; +} + /// Result of an unsuccessful call to [`Compositor::present`]. #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { @@ -119,3 +144,69 @@ pub struct Information { /// Contains the graphics backend. pub backend: String, } + +#[cfg(debug_assertions)] +impl Compositor for () { + type Renderer = (); + type Surface = (); + + async fn with_backend( + _settings: Settings, + _compatible_window: W, + _preffered_backend: Option<&str>, + ) -> Result { + Ok(()) + } + + fn create_renderer(&self) -> Self::Renderer {} + + fn create_surface( + &mut self, + _window: W, + _width: u32, + _height: u32, + ) -> Self::Surface { + } + + fn configure_surface( + &mut self, + _surface: &mut Self::Surface, + _width: u32, + _height: u32, + ) { + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + + fn fetch_information(&self) -> Information { + Information { + adapter: String::from("Null Renderer"), + backend: String::from("Null"), + } + } + + fn present( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + ) -> Result<(), SurfaceError> { + Ok(()) + } + + fn screenshot( + &mut self, + _renderer: &mut Self::Renderer, + _surface: &mut Self::Surface, + _viewport: &Viewport, + _background_color: Color, + ) -> Vec { + vec![] + } +} + +#[cfg(debug_assertions)] +impl Default for () { + type Compositor = (); +} diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 8edf69d7..17d60451 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -1,196 +1,14 @@ -//! Track and compute the damage of graphical primitives. -use crate::core::alignment; -use crate::core::{Rectangle, Size}; -use crate::Primitive; +//! Compute the damage between frames. +use crate::core::{Point, Rectangle}; -use std::sync::Arc; - -/// A type that has some damage bounds. -pub trait Damage: PartialEq { - /// Returns the bounds of the [`Damage`]. - fn bounds(&self) -> Rectangle; -} - -impl Damage for Primitive { - fn bounds(&self) -> Rectangle { - match self { - Self::Text { - bounds, - horizontal_alignment, - vertical_alignment, - .. - } => { - let mut bounds = *bounds; - - bounds.x = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - bounds.y = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - bounds.expand(1.5) - } - Self::Paragraph { - paragraph, - position, - .. - } => { - let mut bounds = - Rectangle::new(*position, paragraph.min_bounds); - - bounds.x = match paragraph.horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - bounds.y = match paragraph.vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - bounds.expand(1.5) - } - Self::Editor { - editor, position, .. - } => { - let bounds = Rectangle::new(*position, editor.bounds); - - bounds.expand(1.5) - } - Self::RawText(raw) => { - // TODO: Add `size` field to `raw` to compute more accurate - // damage bounds (?) - raw.clip_bounds.expand(1.5) - } - Self::Quad { bounds, shadow, .. } if shadow.color.a > 0.0 => { - let bounds_with_shadow = Rectangle { - x: bounds.x + shadow.offset.x.min(0.0) - shadow.blur_radius, - y: bounds.y + shadow.offset.y.min(0.0) - shadow.blur_radius, - width: bounds.width - + shadow.offset.x.abs() - + shadow.blur_radius * 2.0, - height: bounds.height - + shadow.offset.y.abs() - + shadow.blur_radius * 2.0, - }; - - bounds_with_shadow.expand(1.0) - } - Self::Quad { bounds, .. } - | Self::Image { bounds, .. } - | Self::Svg { bounds, .. } => bounds.expand(1.0), - Self::Clip { bounds, .. } => bounds.expand(1.0), - Self::Group { primitives } => primitives - .iter() - .map(Self::bounds) - .fold(Rectangle::with_size(Size::ZERO), |a, b| { - Rectangle::union(&a, &b) - }), - Self::Transform { - transformation, - content, - } => content.bounds() * *transformation, - Self::Cache { content } => content.bounds(), - Self::Custom(custom) => custom.bounds(), - } - } -} - -fn regions(a: &Primitive, b: &Primitive) -> Vec { - match (a, b) { - ( - Primitive::Group { - primitives: primitives_a, - }, - Primitive::Group { - primitives: primitives_b, - }, - ) => return list(primitives_a, primitives_b), - ( - Primitive::Clip { - bounds: bounds_a, - content: content_a, - .. - }, - Primitive::Clip { - bounds: bounds_b, - content: content_b, - .. - }, - ) => { - if bounds_a == bounds_b { - return regions(content_a, content_b) - .into_iter() - .filter_map(|r| r.intersection(&bounds_a.expand(1.0))) - .collect(); - } else { - return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)]; - } - } - ( - Primitive::Transform { - transformation: transformation_a, - content: content_a, - }, - Primitive::Transform { - transformation: transformation_b, - content: content_b, - }, - ) => { - if transformation_a == transformation_b { - return regions(content_a, content_b) - .into_iter() - .map(|r| r * *transformation_a) - .collect(); - } - } - ( - Primitive::Cache { content: content_a }, - Primitive::Cache { content: content_b }, - ) => { - if Arc::ptr_eq(content_a, content_b) { - return vec![]; - } - } - _ if a == b => return vec![], - _ => {} - } - - let bounds_a = a.bounds(); - let bounds_b = b.bounds(); - - if bounds_a == bounds_b { - vec![bounds_a] - } else { - vec![bounds_a, bounds_b] - } -} - -/// Computes the damage regions between the two given lists of primitives. -pub fn list( - previous: &[Primitive], - current: &[Primitive], +/// Diffs the damage regions given some previous and current primitives. +pub fn diff( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + diff: impl Fn(&T, &T) -> Vec, ) -> Vec { - let damage = previous - .iter() - .zip(current) - .flat_map(|(a, b)| regions(a, b)); + let damage = previous.iter().zip(current).flat_map(|(a, b)| diff(a, b)); if previous.len() == current.len() { damage.collect() @@ -203,39 +21,45 @@ pub fn list( // Extend damage by the added/removed primitives damage - .chain(bigger[smaller.len()..].iter().map(Damage::bounds)) + .chain(bigger[smaller.len()..].iter().flat_map(bounds)) .collect() } } +/// Computes the damage regions given some previous and current primitives. +pub fn list( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + are_equal: impl Fn(&T, &T) -> bool, +) -> Vec { + diff(previous, current, &bounds, |a, b| { + if are_equal(a, b) { + vec![] + } else { + bounds(a).into_iter().chain(bounds(b)).collect() + } + }) +} + /// Groups the given damage regions that are close together inside the given /// bounds. -pub fn group( - mut damage: Vec, - scale_factor: f32, - bounds: Size, -) -> Vec { +pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { use std::cmp::Ordering; const AREA_THRESHOLD: f32 = 20_000.0; - let bounds = Rectangle { - x: 0.0, - y: 0.0, - width: bounds.width as f32, - height: bounds.height as f32, - }; - damage.sort_by(|a, b| { - a.x.partial_cmp(&b.x) + a.center() + .distance(Point::ORIGIN) + .partial_cmp(&b.center().distance(Point::ORIGIN)) .unwrap_or(Ordering::Equal) - .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal)) }); let mut output = Vec::new(); let mut scaled = damage .into_iter() - .filter_map(|region| (region * scale_factor).intersection(&bounds)) + .filter_map(|region| region.intersection(&bounds)) .filter(|region| region.width >= 1.0 && region.height >= 1.0); if let Some(mut current) = scaled.next() { diff --git a/graphics/src/error.rs b/graphics/src/error.rs index 77758f54..6ea1d3a4 100644 --- a/graphics/src/error.rs +++ b/graphics/src/error.rs @@ -1,5 +1,7 @@ +//! See what can go wrong when creating graphical backends. + /// An error that occurred while creating an application's graphical context. -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum Error { /// The requested backend version is not supported. #[error("the requested backend version is not supported")] @@ -11,9 +13,30 @@ pub enum Error { /// A suitable graphics adapter or device could not be found. #[error("a suitable graphics adapter or device could not be found")] - GraphicsAdapterNotFound, + GraphicsAdapterNotFound { + /// The name of the backend where the error happened + backend: &'static str, + /// The reason why this backend could not be used + reason: Reason, + }, - /// 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), + + /// Multiple errors occurred + #[error("multiple errors occurred: {0:?}")] + List(Vec), +} + +/// The reason why a graphics adapter could not be found +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Reason { + /// The backend did not match the preference + DidNotMatch { + /// The preferred backend + preferred_backend: String, + }, + /// The request to create the backend failed + RequestFailed(String), } diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index d7d6a0aa..ab4a7a36 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -1,12 +1,16 @@ //! Build and draw geometry. pub mod fill; +pub mod frame; pub mod path; pub mod stroke; +mod cache; mod style; mod text; +pub use cache::Cache; pub use fill::Fill; +pub use frame::Frame; pub use path::Path; pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; @@ -14,11 +18,30 @@ pub use text::Text; pub use crate::gradient::{self, Gradient}; -/// A renderer capable of drawing some [`Self::Geometry`]. -pub trait Renderer: crate::core::Renderer { - /// The kind of geometry this renderer can draw. - type Geometry; +use crate::cache::Cached; +use crate::core::{self, Size}; - /// Draws the given layers of [`Self::Geometry`]. - fn draw(&mut self, layers: Vec); +/// A renderer capable of drawing some [`Self::Geometry`]. +pub trait Renderer: core::Renderer { + /// The kind of geometry this renderer can draw. + type Geometry: Cached; + + /// The kind of [`Frame`] this renderer supports. + type Frame: frame::Backend; + + /// Creates a new [`Self::Frame`]. + fn new_frame(&self, size: Size) -> Self::Frame; + + /// Draws the given [`Self::Geometry`]. + fn draw_geometry(&mut self, geometry: Self::Geometry); +} + +#[cfg(debug_assertions)] +impl Renderer for () { + type Geometry = (); + type Frame = (); + + fn new_frame(&self, _size: Size) -> Self::Frame {} + + fn draw_geometry(&mut self, _geometry: Self::Geometry) {} } diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs new file mode 100644 index 00000000..d70cee0b --- /dev/null +++ b/graphics/src/geometry/cache.rs @@ -0,0 +1,116 @@ +use crate::cache::{self, Cached}; +use crate::core::Size; +use crate::geometry::{self, Frame}; + +pub use cache::Group; + +/// A simple cache that stores generated geometry to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +pub struct Cache +where + Renderer: geometry::Renderer, +{ + raw: crate::Cache::Cache>>, +} + +#[derive(Debug, Clone)] +struct Data { + bounds: Size, + geometry: T, +} + +impl Cache +where + Renderer: geometry::Renderer, +{ + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + raw: cache::Cache::new(), + } + } + + /// Creates a new empty [`Cache`] with the given [`Group`]. + /// + /// Caches within the same group may reuse internal rendering storage. + /// + /// You should generally group caches that are likely to change + /// together. + pub fn with_group(group: Group) -> Self { + Cache { + raw: crate::Cache::with_group(group), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + self.raw.clear(); + } + + /// Draws geometry using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored geometry will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored geometry if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Renderer::Geometry { + use std::ops::Deref; + + let state = self.raw.state(); + + let previous = match state.borrow().deref() { + cache::State::Empty { previous } => { + previous.as_ref().map(|data| data.geometry.clone()) + } + cache::State::Filled { current } => { + if current.bounds == bounds { + return Cached::load(¤t.geometry); + } + + Some(current.geometry.clone()) + } + }; + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let geometry = frame.into_geometry().cache(self.raw.group(), previous); + let result = Cached::load(&geometry); + + *state.borrow_mut() = cache::State::Filled { + current: Data { bounds, geometry }, + }; + + result + } +} + +impl std::fmt::Debug for Cache +where + Renderer: geometry::Renderer, + ::Cache: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", &self.raw) + } +} + +impl Default for Cache +where + Renderer: geometry::Renderer, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs new file mode 100644 index 00000000..377589d7 --- /dev/null +++ b/graphics/src/geometry/frame.rs @@ -0,0 +1,249 @@ +//! Draw and generate geometry. +use crate::core::{Point, Radians, Rectangle, Size, Vector}; +use crate::geometry::{self, Fill, Path, Stroke, Text}; + +/// The region of a surface that can be used to draw geometry. +#[allow(missing_debug_implementations)] +pub struct Frame +where + Renderer: geometry::Renderer, +{ + raw: Renderer::Frame, +} + +impl Frame +where + Renderer: geometry::Renderer, +{ + /// Creates a new [`Frame`] with the given dimensions. + pub fn new(renderer: &Renderer, size: Size) -> Self { + Self { + raw: renderer.new_frame(size), + } + } + + /// Returns the width of the [`Frame`]. + pub fn width(&self) -> f32 { + self.raw.width() + } + + /// Returns the height of the [`Frame`]. + pub fn height(&self) -> f32 { + self.raw.height() + } + + /// Returns the dimensions of the [`Frame`]. + pub fn size(&self) -> Size { + self.raw.size() + } + + /// Returns the coordinate of the center of the [`Frame`]. + pub fn center(&self) -> Point { + self.raw.center() + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + self.raw.fill(path, fill); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + self.raw.fill_rectangle(top_left, size, fill); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + self.raw.stroke(path, stroke); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ All text will be rendered on top of all the layers of + /// a `Canvas`. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + pub fn fill_text(&mut self, text: impl Into) { + self.raw.fill_text(text); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.push_transform(); + + let result = f(self); + + self.pop_transform(); + + result + } + + /// Pushes the current transform in the transform stack. + pub fn push_transform(&mut self) { + self.raw.push_transform(); + } + + /// Pops a transform from the transform stack and sets it as the current transform. + pub fn pop_transform(&mut self) { + self.raw.pop_transform(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip( + &mut self, + region: Rectangle, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + let mut frame = self.draft(region); + + let result = f(&mut frame); + + self.paste(frame, Point::new(region.x, region.y)); + + result + } + + /// Creates a new [`Frame`] with the given [`Size`]. + /// + /// Draw its contents back to this [`Frame`] with [`paste`]. + /// + /// [`paste`]: Self::paste + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self { + raw: self.raw.draft(clip_bounds), + } + } + + /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. + fn paste(&mut self, frame: Self, at: Point) { + self.raw.paste(frame.raw, at); + } + + /// Applies a translation to the current transform of the [`Frame`]. + pub fn translate(&mut self, translation: Vector) { + self.raw.translate(translation); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + pub fn rotate(&mut self, angle: impl Into) { + self.raw.rotate(angle); + } + + /// Applies a uniform scaling to the current transform of the [`Frame`]. + pub fn scale(&mut self, scale: impl Into) { + self.raw.scale(scale); + } + + /// Applies a non-uniform scaling to the current transform of the [`Frame`]. + pub fn scale_nonuniform(&mut self, scale: impl Into) { + self.raw.scale_nonuniform(scale); + } + + /// Turns the [`Frame`] into its underlying geometry. + pub fn into_geometry(self) -> Renderer::Geometry { + self.raw.into_geometry() + } +} + +/// The internal implementation of a [`Frame`]. +/// +/// Analogous to [`Frame`]. See [`Frame`] for the documentation +/// of each method. +#[allow(missing_docs)] +pub trait Backend: Sized { + type Geometry; + + fn width(&self) -> f32; + fn height(&self) -> f32; + fn size(&self) -> Size; + fn center(&self) -> Point; + + fn push_transform(&mut self); + fn pop_transform(&mut self); + + fn translate(&mut self, translation: Vector); + fn rotate(&mut self, angle: impl Into); + fn scale(&mut self, scale: impl Into); + fn scale_nonuniform(&mut self, scale: impl Into); + + fn draft(&mut self, clip_bounds: Rectangle) -> Self; + fn paste(&mut self, frame: Self, at: Point); + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); + + fn fill(&mut self, path: &Path, fill: impl Into); + fn fill_text(&mut self, text: impl Into); + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ); + + fn into_geometry(self) -> Self::Geometry; +} + +#[cfg(debug_assertions)] +impl Backend for () { + type Geometry = (); + + fn width(&self) -> f32 { + 0.0 + } + + fn height(&self) -> f32 { + 0.0 + } + + fn size(&self) -> Size { + Size::ZERO + } + + fn center(&self) -> Point { + Point::ORIGIN + } + + fn push_transform(&mut self) {} + fn pop_transform(&mut self) {} + + fn translate(&mut self, _translation: Vector) {} + fn rotate(&mut self, _angle: impl Into) {} + fn scale(&mut self, _scale: impl Into) {} + fn scale_nonuniform(&mut self, _scale: impl Into) {} + + fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} + fn paste(&mut self, _frame: Self, _at: Point) {} + + fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} + + fn fill(&mut self, _path: &Path, _fill: impl Into) {} + fn fill_text(&mut self, _text: impl Into) {} + fn fill_rectangle( + &mut self, + _top_left: Point, + _size: Size, + _fill: impl Into, + ) { + } + + fn into_geometry(self) -> Self::Geometry {} +} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index d89caace..318592be 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -1,14 +1,121 @@ //! Load and operate on images. -use crate::core::image::{Data, Handle}; - -use bitflags::bitflags; - +#[cfg(feature = "image")] pub use ::image as image_rs; +use crate::core::{image, svg, Color, Radians, Rectangle}; + +/// A raster or vector image. +#[derive(Debug, Clone, PartialEq)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The filter method of a raster image. + filter_method: image::FilterMethod, + + /// The bounds of the image. + bounds: Rectangle, + + /// The rotation of the image. + rotation: Radians, + + /// The opacity of the image. + opacity: f32, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option, + + /// The bounds of the image. + bounds: Rectangle, + + /// The rotation of the image. + rotation: Radians, + + /// The opacity of the image. + opacity: f32, + }, +} + +impl Image { + /// Returns the bounds of the [`Image`]. + pub fn bounds(&self) -> Rectangle { + match self { + Image::Raster { + bounds, rotation, .. + } + | Image::Vector { + bounds, rotation, .. + } => bounds.rotate(*rotation), + } + } +} + +#[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. -pub fn load(handle: &Handle) -> image_rs::ImageResult { - match handle.data() { - Data::Path(path) => { +/// +/// [`Handle`]: image::Handle +pub fn load( + handle: &image::Handle, +) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba, image::Bytes>> +{ + use bitflags::bitflags; + + bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b001; + const ROTATE_180 = 0b010; + const FLIP_DIAGONALLY = 0b100; + } + } + + impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif(reader: &mut R) -> Result + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .and_then(|value| Self::from_bits(value.saturating_sub(1))) + .unwrap_or_else(Self::empty)) + } + + fn perform( + self, + mut image: ::image::DynamicImage, + ) -> ::image::DynamicImage { + use ::image::imageops; + + if self.contains(Self::FLIP_DIAGONALLY) { + imageops::flip_vertical_in_place(&mut image); + } + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } + } + + let (width, height, pixels) = match handle { + image::Handle::Path(_, path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -17,79 +124,44 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult { .and_then(|mut reader| Operation::from_exif(&mut reader).ok()) .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - Data::Bytes(bytes) => { + image::Handle::Bytes(_, bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) .ok() .unwrap_or_else(Operation::empty); - Ok(operation.perform(image)) + let rgba = operation.perform(image).into_rgba8(); + + ( + rgba.width(), + rgba.height(), + image::Bytes::from(rgba.into_raw()), + ) } - Data::Rgba { + image::Handle::Rgba { width, height, pixels, - } => { - if let Some(image) = image_rs::ImageBuffer::from_vec( - *width, - *height, - pixels.to_vec(), - ) { - Ok(image_rs::DynamicImage::ImageRgba8(image)) - } else { - Err(image_rs::error::ImageError::Limits( - image_rs::error::LimitError::from_kind( - image_rs::error::LimitErrorKind::DimensionError, - ), - )) - } - } - } -} - -bitflags! { - struct Operation: u8 { - const FLIP_HORIZONTALLY = 0b001; - const ROTATE_180 = 0b010; - const FLIP_DIAGONALLY = 0b100; - } -} - -impl Operation { - // Meaning of the returned value is described e.g. at: - // https://magnushoff.com/articles/jpeg-orientation/ - fn from_exif(reader: &mut R) -> Result - where - R: std::io::BufRead + std::io::Seek, - { - let exif = exif::Reader::new().read_from_container(reader)?; - - Ok(exif - .get_field(exif::Tag::Orientation, exif::In::PRIMARY) - .and_then(|field| field.value.get_uint(0)) - .and_then(|value| u8::try_from(value).ok()) - .and_then(|value| Self::from_bits(value.saturating_sub(1))) - .unwrap_or_else(Self::empty)) - } - - fn perform(self, mut image: image::DynamicImage) -> image::DynamicImage { - use image::imageops; - - if self.contains(Self::FLIP_DIAGONALLY) { - imageops::flip_vertical_in_place(&mut image); - } - - if self.contains(Self::ROTATE_180) { - imageops::rotate180_in_place(&mut image); - } - - if self.contains(Self::FLIP_HORIZONTALLY) { - imageops::flip_horizontal_in_place(&mut image); - } - - image + .. + } => (*width, *height, pixels.clone()), + }; + + if let Some(image) = ::image::ImageBuffer::from_raw(width, height, pixels) { + Ok(image) + } else { + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, + ), + )) } } diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..c9a818fb --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,144 @@ +//! Draw and stack layers of graphical primitives. +use crate::core::{Rectangle, Transformation}; + +/// A layer of graphical primitives. +/// +/// Layers normally dictate a set of primitives that are +/// rendered in a specific order. +pub trait Layer: Default { + /// Creates a new [`Layer`] with the given bounds. + fn with_bounds(bounds: Rectangle) -> Self; + + /// Flushes and settles any pending group of primitives in the [`Layer`]. + /// + /// This will be called when a [`Layer`] is finished. It allows layers to efficiently + /// record primitives together and defer grouping until the end. + fn flush(&mut self); + + /// Resizes the [`Layer`] to the given bounds. + fn resize(&mut self, bounds: Rectangle); + + /// Clears all the layers contents and resets its bounds. + fn reset(&mut self); +} + +/// A stack of layers used for drawing. +#[derive(Debug)] +pub struct Stack { + layers: Vec, + transformations: Vec, + previous: Vec, + current: usize, + active_count: usize, +} + +impl Stack { + /// Creates a new empty [`Stack`]. + pub fn new() -> Self { + Self { + layers: vec![T::default()], + transformations: vec![Transformation::IDENTITY], + previous: vec![], + current: 0, + active_count: 1, + } + } + + /// Returns a mutable reference to the current [`Layer`] of the [`Stack`], together with + /// the current [`Transformation`]. + #[inline] + pub fn current_mut(&mut self) -> (&mut T, Transformation) { + let transformation = self.transformation(); + + (&mut self.layers[self.current], transformation) + } + + /// Returns the current [`Transformation`] of the [`Stack`]. + #[inline] + pub fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + /// Pushes a new clipping region in the [`Stack`]; creating a new layer in the + /// process. + pub fn push_clip(&mut self, bounds: Rectangle) { + self.previous.push(self.current); + + self.current = self.active_count; + self.active_count += 1; + + let bounds = bounds * self.transformation(); + + if self.current == self.layers.len() { + self.layers.push(T::with_bounds(bounds)); + } else { + self.layers[self.current].resize(bounds); + } + } + + /// Pops the current clipping region from the [`Stack`] and restores the previous one. + /// + /// The current layer will be recorded for drawing. + pub fn pop_clip(&mut self) { + self.flush(); + + self.current = self.previous.pop().unwrap(); + } + + /// Pushes a new [`Transformation`] in the [`Stack`]. + /// + /// Future drawing operations will be affected by this new [`Transformation`] until + /// it is popped using [`pop_transformation`]. + /// + /// [`pop_transformation`]: Self::pop_transformation + pub fn push_transformation(&mut self, transformation: Transformation) { + self.transformations + .push(self.transformation() * transformation); + } + + /// Pops the current [`Transformation`] in the [`Stack`]. + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + /// Returns an iterator over mutable references to the layers in the [`Stack`]. + pub fn iter_mut(&mut self) -> impl Iterator { + self.flush(); + + self.layers[..self.active_count].iter_mut() + } + + /// Returns an iterator over immutable references to the layers in the [`Stack`]. + pub fn iter(&self) -> impl Iterator { + self.layers[..self.active_count].iter() + } + + /// Returns the slice of layers in the [`Stack`]. + pub fn as_slice(&self) -> &[T] { + &self.layers[..self.active_count] + } + + /// Flushes and settles any primitives in the current layer of the [`Stack`]. + pub fn flush(&mut self) { + self.layers[self.current].flush(); + } + + /// Clears the layers of the [`Stack`], allowing reuse. + /// + /// This will normally keep layer allocations for future drawing operations. + pub fn clear(&mut self) { + for layer in self.layers[..self.active_count].iter_mut() { + layer.reset(); + } + + self.current = 0; + self.active_count = 1; + self.previous.clear(); + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index aa9d00e8..b5ef55e7 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -7,44 +7,35 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unsafe_code, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; -mod error; -mod primitive; +mod settings; mod viewport; -pub mod backend; +pub mod cache; pub mod color; pub mod compositor; pub mod damage; +pub mod error; pub mod gradient; +pub mod image; +pub mod layer; pub mod mesh; -pub mod renderer; pub mod text; #[cfg(feature = "geometry")] pub mod geometry; -#[cfg(feature = "image")] -pub mod image; - pub use antialiasing::Antialiasing; -pub use backend::Backend; +pub use cache::Cache; pub use compositor::Compositor; -pub use damage::Damage; pub use error::Error; pub use gradient::Gradient; +pub use image::Image; +pub use layer::Layer; pub use mesh::Mesh; -pub use primitive::Primitive; -pub use renderer::Renderer; +pub use settings::Settings; +pub use text::Text; pub use viewport::Viewport; pub use iced_core as core; diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs index 041986cf..76602319 100644 --- a/graphics/src/mesh.rs +++ b/graphics/src/mesh.rs @@ -1,8 +1,7 @@ //! Draw triangles! use crate::color; -use crate::core::{Rectangle, Size}; +use crate::core::{Rectangle, Transformation}; use crate::gradient; -use crate::Damage; use bytemuck::{Pod, Zeroable}; @@ -14,29 +13,55 @@ pub enum Mesh { /// The vertices and indices of the mesh. buffers: Indexed, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. - size: Size, + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, /// A mesh with a gradient. Gradient { /// The vertices and indices of the mesh. buffers: Indexed, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. - size: Size, + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, } -impl Damage for Mesh { - fn bounds(&self) -> Rectangle { +impl Mesh { + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { match self { - Self::Solid { size, .. } | Self::Gradient { size, .. } => { - Rectangle::with_size(*size) + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the [`Transformation`] of the [`Mesh`]. + pub fn transformation(&self) -> Transformation { + match self { + Self::Solid { transformation, .. } + | Self::Gradient { transformation, .. } => *transformation, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { + match self { + Self::Solid { + clip_bounds, + transformation, + .. } + | Self::Gradient { + clip_bounds, + transformation, + .. + } => *clip_bounds * *transformation, } } } @@ -74,3 +99,50 @@ pub struct GradientVertex2D { /// The packed vertex data of the gradient. pub gradient: gradient::Packed, } + +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of solid meshes. + pub solids: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of gradient meshes. + pub gradients: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of(meshes: &[Mesh]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solids += 1; + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradients += 1; + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} + +/// A renderer capable of drawing a [`Mesh`]. +pub trait Renderer { + /// Draws the given [`Mesh`]. + fn draw_mesh(&mut self, mesh: Mesh); +} diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs deleted file mode 100644 index 6929b0a1..00000000 --- a/graphics/src/primitive.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Draw using different graphical primitives. -use crate::core::alignment; -use crate::core::image; -use crate::core::svg; -use crate::core::text; -use crate::core::{ - Background, Border, Color, Font, Pixels, Point, Rectangle, Shadow, - Transformation, Vector, -}; -use crate::text::editor; -use crate::text::paragraph; - -use std::sync::Arc; - -/// A rendering primitive. -#[derive(Debug, Clone, PartialEq)] -pub enum Primitive { - /// A text primitive - Text { - /// The contents of the text. - content: String, - /// The bounds of the text. - bounds: Rectangle, - /// The color of the text. - color: Color, - /// The size of the text in logical pixels. - size: Pixels, - /// The line height of the text. - line_height: text::LineHeight, - /// The font of the text. - font: Font, - /// The horizontal alignment of the text. - horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text. - vertical_alignment: alignment::Vertical, - /// The shaping strategy of the text. - shaping: text::Shaping, - /// The clip bounds of the text. - clip_bounds: Rectangle, - }, - /// A paragraph primitive - Paragraph { - /// The [`paragraph::Weak`] reference. - paragraph: paragraph::Weak, - /// The position of the paragraph. - position: Point, - /// The color of the paragraph. - color: Color, - /// The clip bounds of the paragraph. - clip_bounds: Rectangle, - }, - /// An editor primitive - Editor { - /// The [`editor::Weak`] reference. - editor: editor::Weak, - /// The position of the editor. - position: Point, - /// The color of the editor. - color: Color, - /// The clip bounds of the editor. - clip_bounds: Rectangle, - }, - /// A raw `cosmic-text` primitive - RawText(crate::text::Raw), - /// A quad primitive - Quad { - /// The bounds of the quad - bounds: Rectangle, - /// The background of the quad - background: Background, - /// The [`Border`] of the quad - border: Border, - /// The [`Shadow`] of the quad - shadow: Shadow, - }, - /// An image primitive - Image { - /// The handle of the image - handle: image::Handle, - /// The filter method of the image - filter_method: image::FilterMethod, - /// The bounds of the image - bounds: Rectangle, - }, - /// An SVG primitive - Svg { - /// The path of the SVG file - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the viewport - bounds: Rectangle, - }, - /// A group of primitives - Group { - /// The primitives of the group - primitives: Vec>, - }, - /// A clip primitive - Clip { - /// The bounds of the clip - bounds: Rectangle, - /// The content of the clip - content: Box>, - }, - /// A primitive that applies a [`Transformation`] - Transform { - /// The [`Transformation`] - transformation: Transformation, - - /// The primitive to transform - content: Box>, - }, - /// A cached primitive. - /// - /// This can be useful if you are implementing a widget where primitive - /// generation is expensive. - Cache { - /// The cached primitive - content: Arc>, - }, - /// A backend-specific primitive. - Custom(T), -} - -impl Primitive { - /// Groups the current [`Primitive`]. - pub fn group(primitives: Vec) -> Self { - Self::Group { primitives } - } - - /// Clips the current [`Primitive`]. - pub fn clip(self, bounds: Rectangle) -> Self { - Self::Clip { - bounds, - content: Box::new(self), - } - } - - /// Translates the current [`Primitive`]. - pub fn translate(self, translation: Vector) -> Self { - Self::Transform { - transformation: Transformation::translate( - translation.x, - translation.y, - ), - content: Box::new(self), - } - } - - /// Transforms the current [`Primitive`]. - pub fn transform(self, transformation: Transformation) -> Self { - Self::Transform { - transformation, - content: Box::new(self), - } - } -} diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs deleted file mode 100644 index 143f348b..00000000 --- a/graphics/src/renderer.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Create a renderer from a [`Backend`]. -use crate::backend::{self, Backend}; -use crate::core; -use crate::core::image; -use crate::core::renderer; -use crate::core::svg; -use crate::core::text::Text; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, -}; -use crate::text; -use crate::Primitive; - -use std::borrow::Cow; - -/// A backend-agnostic renderer that supports all the built-in widgets. -#[derive(Debug)] -pub struct Renderer { - backend: B, - default_font: Font, - default_text_size: Pixels, - primitives: Vec>, -} - -impl Renderer { - /// Creates a new [`Renderer`] from the given [`Backend`]. - pub fn new( - backend: B, - default_font: Font, - default_text_size: Pixels, - ) -> Self { - Self { - backend, - default_font, - default_text_size, - primitives: Vec::new(), - } - } - - /// Returns a reference to the [`Backend`] of the [`Renderer`]. - pub fn backend(&self) -> &B { - &self.backend - } - - /// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing. - pub fn draw_primitive(&mut self, primitive: Primitive) { - self.primitives.push(primitive); - } - - /// Runs the given closure with the [`Backend`] and the recorded primitives - /// of the [`Renderer`]. - pub fn with_primitives( - &mut self, - f: impl FnOnce(&mut B, &[Primitive]) -> O, - ) -> O { - f(&mut self.backend, &self.primitives) - } - - /// Starts recording a new layer. - pub fn start_layer(&mut self) -> Vec> { - std::mem::take(&mut self.primitives) - } - - /// Ends the recording of a layer. - pub fn end_layer( - &mut self, - primitives: Vec>, - bounds: Rectangle, - ) { - let layer = std::mem::replace(&mut self.primitives, primitives); - - self.primitives.push(Primitive::group(layer).clip(bounds)); - } - - /// Starts recording a translation. - pub fn start_transformation(&mut self) -> Vec> { - std::mem::take(&mut self.primitives) - } - - /// Ends the recording of a translation. - pub fn end_transformation( - &mut self, - primitives: Vec>, - transformation: Transformation, - ) { - let layer = std::mem::replace(&mut self.primitives, primitives); - - self.primitives - .push(Primitive::group(layer).transform(transformation)); - } -} - -impl iced_core::Renderer for Renderer { - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - let current = self.start_layer(); - - f(self); - - self.end_layer(current, bounds); - } - - fn with_transformation( - &mut self, - transformation: Transformation, - f: impl FnOnce(&mut Self), - ) { - let current = self.start_transformation(); - - f(self); - - self.end_transformation(current, transformation); - } - - fn fill_quad( - &mut self, - quad: renderer::Quad, - background: impl Into, - ) { - self.primitives.push(Primitive::Quad { - bounds: quad.bounds, - background: background.into(), - border: quad.border, - shadow: quad.shadow, - }); - } - - fn clear(&mut self) { - self.primitives.clear(); - } -} - -impl core::text::Renderer for Renderer -where - B: Backend + backend::Text, -{ - type Font = Font; - type Paragraph = text::Paragraph; - type Editor = text::Editor; - - const ICON_FONT: Font = Font::with_name("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{f00c}'; - const ARROW_DOWN_ICON: char = '\u{e800}'; - - fn default_font(&self) -> Self::Font { - self.default_font - } - - fn default_size(&self) -> Pixels { - self.default_text_size - } - - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.backend.load_font(bytes); - } - - fn fill_paragraph( - &mut self, - paragraph: &Self::Paragraph, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Paragraph { - paragraph: paragraph.downgrade(), - position, - color, - clip_bounds, - }); - } - - fn fill_editor( - &mut self, - editor: &Self::Editor, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Editor { - editor: editor.downgrade(), - position, - color, - clip_bounds, - }); - } - - fn fill_text( - &mut self, - text: Text<'_, Self::Font>, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Text { - content: text.content.to_string(), - bounds: Rectangle::new(position, text.bounds), - size: text.size, - line_height: text.line_height, - color, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds, - }); - } -} - -impl image::Renderer for Renderer -where - B: Backend + backend::Image, -{ - type Handle = image::Handle; - - fn dimensions(&self, handle: &image::Handle) -> Size { - self.backend().dimensions(handle) - } - - fn draw( - &mut self, - handle: image::Handle, - filter_method: image::FilterMethod, - bounds: Rectangle, - ) { - self.primitives.push(Primitive::Image { - handle, - filter_method, - bounds, - }); - } -} - -impl svg::Renderer for Renderer -where - B: Backend + backend::Svg, -{ - fn dimensions(&self, handle: &svg::Handle) -> Size { - self.backend().viewport_dimensions(handle) - } - - fn draw( - &mut self, - handle: svg::Handle, - color: Option, - bounds: Rectangle, - ) { - self.primitives.push(Primitive::Svg { - handle, - color, - bounds, - }); - } -} diff --git a/renderer/src/settings.rs b/graphics/src/settings.rs similarity index 91% rename from renderer/src/settings.rs rename to graphics/src/settings.rs index 432eb8a0..2e8275c6 100644 --- a/renderer/src/settings.rs +++ b/graphics/src/settings.rs @@ -1,7 +1,7 @@ use crate::core::{Font, Pixels}; -use crate::graphics::Antialiasing; +use crate::Antialiasing; -/// The settings of a Backend. +/// The settings of a renderer. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { /// The default [`Font`] to use. diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 0310ead7..30269e69 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -9,14 +9,141 @@ pub use paragraph::Paragraph; pub use cosmic_text; +use crate::core::alignment; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::{Color, Point, Rectangle, Size}; +use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock, Weak}; +/// A text primitive. +#[derive(Debug, Clone, PartialEq)] +pub enum Text { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { + paragraph: paragraph::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// Some cached text. + Cached { + /// The contents of the text. + content: String, + /// The bounds of the text. + bounds: Rectangle, + /// The color of the text. + color: Color, + /// The size of the text in logical pixels. + size: Pixels, + /// The line height of the text. + line_height: Pixels, + /// The font of the text. + font: Font, + /// The horizontal alignment of the text. + horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text. + vertical_alignment: alignment::Vertical, + /// The shaping strategy of the text. + shaping: Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, + }, + /// Some raw text. + #[allow(missing_docs)] + Raw { + raw: Raw, + transformation: Transformation, + }, +} + +impl Text { + /// Returns the visible bounds of the [`Text`]. + pub fn visible_bounds(&self) -> Option { + let (bounds, horizontal_alignment, vertical_alignment) = match self { + Text::Paragraph { + position, + paragraph, + clip_bounds, + transformation, + .. + } => ( + Rectangle::new(*position, paragraph.min_bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + Some(paragraph.horizontal_alignment), + Some(paragraph.vertical_alignment), + ), + Text::Editor { + editor, + position, + clip_bounds, + transformation, + .. + } => ( + Rectangle::new(*position, editor.bounds) + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), + None, + None, + ), + Text::Cached { + bounds, + clip_bounds, + horizontal_alignment, + vertical_alignment, + .. + } => ( + bounds.intersection(clip_bounds), + Some(*horizontal_alignment), + Some(*vertical_alignment), + ), + Text::Raw { raw, .. } => (Some(raw.clip_bounds), None, None), + }; + + let mut bounds = bounds?; + + if let Some(alignment) = horizontal_alignment { + match alignment { + alignment::Horizontal::Left => {} + alignment::Horizontal::Center => { + bounds.x -= bounds.width / 2.0; + } + alignment::Horizontal::Right => { + bounds.x -= bounds.width; + } + } + } + + if let Some(alignment) = vertical_alignment { + match alignment { + alignment::Vertical::Top => {} + alignment::Vertical::Center => { + bounds.y -= bounds.height / 2.0; + } + alignment::Vertical::Bottom => { + bounds.y -= bounds.height; + } + } + } + + Some(bounds) + } +} + /// The regular variant of the [Fira Sans] font. /// /// It is loaded as part of the default fonts in Wasm builds. diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 7fb33567..822b61c4 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -2,22 +2,18 @@ use crate::core::{Font, Size}; use crate::text; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; use std::collections::hash_map; -use std::hash::{BuildHasher, Hash, Hasher}; +use std::hash::{Hash, Hasher}; /// A store of recently used sections of text. -#[allow(missing_debug_implementations)] -#[derive(Default)] +#[derive(Debug, Default)] pub struct Cache { entries: FxHashMap, aliases: FxHashMap, recently_used: FxHashSet, - hasher: HashBuilder, } -type HashBuilder = xxhash_rust::xxh3::Xxh3Builder; - impl Cache { /// Creates a new empty [`Cache`]. pub fn new() -> Self { @@ -35,7 +31,7 @@ impl Cache { font_system: &mut cosmic_text::FontSystem, key: Key<'_>, ) -> (KeyHash, &mut Entry) { - let hash = key.hash(self.hasher.build_hasher()); + let hash = key.hash(FxHasher::default()); if let Some(hash) = self.aliases.get(&hash) { let _ = self.recently_used.insert(*hash); @@ -77,7 +73,7 @@ impl Cache { ] { if key.bounds != bounds { let _ = self.aliases.insert( - Key { bounds, ..key }.hash(self.hasher.build_hasher()), + Key { bounds, ..key }.hash(FxHasher::default()), hash, ); } @@ -138,7 +134,7 @@ impl Key<'_> { pub type KeyHash = u64; /// A cache entry. -#[allow(missing_debug_implementations)] +#[derive(Debug)] pub struct Entry { /// The buffer of text, ready for drawing. pub buffer: cosmic_text::Buffer, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c..4b8f0f2a 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -456,10 +456,14 @@ impl editor::Editor for Editor { } } Action::Scroll { lines } => { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { lines }, - ); + let (_, height) = editor.buffer().size(); + + if height < i32::MAX as f32 { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } } } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 5d027542..31a323ac 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -61,7 +61,7 @@ impl Paragraph { impl core::text::Paragraph for Paragraph { type Font = Font; - fn with_text(text: Text<'_, Font>) -> Self { + fn with_text(text: Text<&str>) -> Self { log::trace!("Allocating paragraph: {}", text.content); let mut font_system = @@ -146,7 +146,7 @@ impl core::text::Paragraph for Paragraph { } } - fn compare(&self, text: Text<'_, Font>) -> core::text::Difference { + fn compare(&self, text: Text<&str>) -> core::text::Difference { let font_system = text::font_system().read().expect("Read font system"); let paragraph = self.internal(); let metrics = paragraph.buffer.metrics(); diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 2d108d6f..7962b89d 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [dependencies] iced_core.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 63f21fc0..7636a712 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -1,3 +1,4 @@ +//! A syntax highlighter for iced. use iced_core as core; use crate::core::text::highlighter::{self, Format}; @@ -16,6 +17,8 @@ static THEMES: Lazy = const LINES_PER_SNAPSHOT: usize = 50; +/// A syntax highlighter. +#[derive(Debug)] pub struct Highlighter { syntax: &'static parsing::SyntaxReference, highlighter: highlighting::Highlighter<'static>, @@ -131,25 +134,47 @@ impl highlighter::Highlighter for Highlighter { } } +/// The settings of a [`Highlighter`]. #[derive(Debug, Clone, PartialEq)] pub struct Settings { + /// The [`Theme`] of the [`Highlighter`]. + /// + /// It dictates the color scheme that will be used for highlighting. pub theme: Theme, + /// The extension of the file to highlight. + /// + /// The [`Highlighter`] will use the extension to automatically determine + /// the grammar to use for highlighting. pub extension: String, } +/// A highlight produced by a [`Highlighter`]. +#[derive(Debug)] pub struct Highlight(highlighting::StyleModifier); impl Highlight { + /// Returns the color of this [`Highlight`]. + /// + /// If `None`, the original text color should be unchanged. pub fn color(&self) -> Option { self.0.foreground.map(|color| { Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0) }) } + /// Returns the font of this [`Highlight`]. + /// + /// If `None`, the original font should be unchanged. pub fn font(&self) -> Option { None } + /// Returns the [`Format`] of the [`Highlight`]. + /// + /// It contains both the [`color`] and the [`font`]. + /// + /// [`color`]: Self::color + /// [`font`]: Self::font pub fn to_format(&self) -> Format { Format { color: self.color(), @@ -158,6 +183,8 @@ impl Highlight { } } +/// A highlighting theme. +#[allow(missing_docs)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Theme { SolarizedDark, @@ -168,6 +195,7 @@ pub enum Theme { } impl Theme { + /// A static slice containing all the available themes. pub const ALL: &'static [Self] = &[ Self::SolarizedDark, Self::Base16Mocha, @@ -176,6 +204,7 @@ impl Theme { Self::InspiredGitHub, ]; + /// Returns `true` if the [`Theme`] is dark, and false otherwise. pub fn is_dark(self) -> bool { match self { Self::SolarizedDark @@ -209,7 +238,7 @@ impl std::fmt::Display for Theme { } } -pub struct ScopeRangeIterator { +struct ScopeRangeIterator { ops: Vec<(usize, parsing::ScopeStackOp)>, line_length: usize, index: usize, diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 5cce2427..458681dd 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -10,19 +10,24 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [features] wgpu = ["iced_wgpu"] -image = ["iced_tiny_skia/image", "iced_wgpu?/image"] -svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"] -geometry = ["iced_graphics/geometry", "iced_tiny_skia/geometry", "iced_wgpu?/geometry"] -tracing = ["iced_wgpu?/tracing"] +tiny-skia = ["iced_tiny_skia"] +image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] +svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] +geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] [dependencies] iced_graphics.workspace = true + iced_tiny_skia.workspace = true +iced_tiny_skia.optional = true iced_wgpu.workspace = true iced_wgpu.optional = true diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs deleted file mode 100644 index 0777f6e4..00000000 --- a/renderer/src/compositor.rs +++ /dev/null @@ -1,259 +0,0 @@ -use crate::core::Color; -use crate::graphics::compositor::{Information, SurfaceError, Window}; -use crate::graphics::{Error, Viewport}; -use crate::{Renderer, Settings}; - -use std::env; - -pub enum Compositor { - TinySkia(iced_tiny_skia::window::Compositor), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Compositor), -} - -pub enum Surface { - TinySkia(iced_tiny_skia::window::Surface), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Surface<'static>), -} - -impl crate::graphics::Compositor for Compositor { - type Settings = Settings; - type Renderer = Renderer; - type Surface = Surface; - - fn new( - settings: Self::Settings, - compatible_window: W, - ) -> Result { - let candidates = - Candidate::list_from_env().unwrap_or(Candidate::default_list()); - - let mut error = Error::GraphicsAdapterNotFound; - - for candidate in candidates { - match candidate.build(settings, compatible_window.clone()) { - Ok(compositor) => return Ok(compositor), - Err(new_error) => { - error = new_error; - } - } - } - - Err(error) - } - - fn create_renderer(&self) -> Self::Renderer { - match self { - Compositor::TinySkia(compositor) => { - Renderer::TinySkia(compositor.create_renderer()) - } - #[cfg(feature = "wgpu")] - Compositor::Wgpu(compositor) => { - Renderer::Wgpu(compositor.create_renderer()) - } - } - } - - fn create_surface( - &mut self, - window: W, - width: u32, - height: u32, - ) -> Surface { - match self { - Self::TinySkia(compositor) => Surface::TinySkia( - compositor.create_surface(window, width, height), - ), - #[cfg(feature = "wgpu")] - Self::Wgpu(compositor) => { - Surface::Wgpu(compositor.create_surface(window, width, height)) - } - } - } - - fn configure_surface( - &mut self, - surface: &mut Surface, - width: u32, - height: u32, - ) { - match (self, surface) { - (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { - compositor.configure_surface(surface, width, height); - } - #[cfg(feature = "wgpu")] - (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { - compositor.configure_surface(surface, width, height); - } - #[allow(unreachable_patterns)] - _ => panic!( - "The provided surface is not compatible with the compositor." - ), - } - } - - fn fetch_information(&self) -> Information { - match self { - Self::TinySkia(compositor) => compositor.fetch_information(), - #[cfg(feature = "wgpu")] - Self::Wgpu(compositor) => compositor.fetch_information(), - } - } - - fn present( - &mut self, - renderer: &mut Self::Renderer, - surface: &mut Self::Surface, - viewport: &Viewport, - background_color: Color, - ) -> Result<(), SurfaceError> { - match (self, renderer, surface) { - ( - Self::TinySkia(_compositor), - crate::Renderer::TinySkia(renderer), - Surface::TinySkia(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_tiny_skia::window::compositor::present( - backend, - surface, - primitives, - viewport, - background_color, - ) - }), - #[cfg(feature = "wgpu")] - ( - Self::Wgpu(compositor), - crate::Renderer::Wgpu(renderer), - Surface::Wgpu(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_wgpu::window::compositor::present( - compositor, - backend, - surface, - primitives, - viewport, - background_color, - ) - }), - #[allow(unreachable_patterns)] - _ => panic!( - "The provided renderer or surface are not compatible \ - with the compositor." - ), - } - } - - fn screenshot( - &mut self, - renderer: &mut Self::Renderer, - surface: &mut Self::Surface, - viewport: &Viewport, - background_color: Color, - ) -> Vec { - match (self, renderer, surface) { - ( - Self::TinySkia(_compositor), - Renderer::TinySkia(renderer), - Surface::TinySkia(surface), - ) => renderer.with_primitives(|backend, primitives| { - iced_tiny_skia::window::compositor::screenshot( - surface, - backend, - primitives, - viewport, - background_color, - ) - }), - #[cfg(feature = "wgpu")] - ( - Self::Wgpu(compositor), - Renderer::Wgpu(renderer), - Surface::Wgpu(_), - ) => renderer.with_primitives(|backend, primitives| { - iced_wgpu::window::compositor::screenshot( - compositor, - backend, - primitives, - viewport, - background_color, - ) - }), - #[allow(unreachable_patterns)] - _ => panic!( - "The provided renderer or backend are not compatible \ - with the compositor." - ), - } - } -} - -enum Candidate { - Wgpu, - TinySkia, -} - -impl Candidate { - fn default_list() -> Vec { - vec![ - #[cfg(feature = "wgpu")] - Self::Wgpu, - Self::TinySkia, - ] - } - - fn list_from_env() -> Option> { - let backends = env::var("ICED_BACKEND").ok()?; - - Some( - backends - .split(',') - .map(str::trim) - .map(|backend| match backend { - "wgpu" => Self::Wgpu, - "tiny-skia" => Self::TinySkia, - _ => panic!("unknown backend value: \"{backend}\""), - }) - .collect(), - ) - } - - fn build( - self, - settings: Settings, - _compatible_window: W, - ) -> Result { - match self { - Self::TinySkia => { - let compositor = iced_tiny_skia::window::compositor::new( - iced_tiny_skia::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - }, - _compatible_window, - ); - - Ok(Compositor::TinySkia(compositor)) - } - #[cfg(feature = "wgpu")] - Self::Wgpu => { - let compositor = iced_wgpu::window::compositor::new( - iced_wgpu::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - ..iced_wgpu::Settings::from_env() - }, - _compatible_window, - )?; - - Ok(Compositor::Wgpu(compositor)) - } - #[cfg(not(feature = "wgpu"))] - Self::Wgpu => { - panic!("`wgpu` feature was not enabled in `iced_renderer`") - } - } - } -} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs new file mode 100644 index 00000000..a0f4f40e --- /dev/null +++ b/renderer/src/fallback.rs @@ -0,0 +1,631 @@ +//! Compose existing renderers and create type-safe fallback strategies. +use crate::core::image; +use crate::core::renderer; +use crate::core::svg; +use crate::core::{ + self, Background, Color, Point, Radians, Rectangle, Size, Transformation, +}; +use crate::graphics; +use crate::graphics::compositor; +use crate::graphics::mesh; + +use std::borrow::Cow; + +/// A renderer `A` with a fallback strategy `B`. +/// +/// This type can be used to easily compose existing renderers and +/// create custom, type-safe fallback strategies. +#[derive(Debug)] +pub enum Renderer { + /// The primary rendering option. + Primary(A), + /// The secondary (or fallback) rendering option. + Secondary(B), +} + +macro_rules! delegate { + ($renderer:expr, $name:ident, $body:expr) => { + match $renderer { + Self::Primary($name) => $body, + Self::Secondary($name) => $body, + } + }; +} + +impl core::Renderer for Renderer +where + A: core::Renderer, + B: core::Renderer, +{ + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into, + ) { + delegate!(self, renderer, renderer.fill_quad(quad, background.into())); + } + + fn clear(&mut self) { + delegate!(self, renderer, renderer.clear()); + } + + fn start_layer(&mut self, bounds: Rectangle) { + delegate!(self, renderer, renderer.start_layer(bounds)); + } + + fn end_layer(&mut self) { + delegate!(self, renderer, renderer.end_layer()); + } + + fn start_transformation(&mut self, transformation: Transformation) { + delegate!( + self, + renderer, + renderer.start_transformation(transformation) + ); + } + + fn end_transformation(&mut self) { + delegate!(self, renderer, renderer.end_transformation()); + } +} + +impl core::text::Renderer for Renderer +where + A: core::text::Renderer, + B: core::text::Renderer< + Font = A::Font, + Paragraph = A::Paragraph, + Editor = A::Editor, + >, +{ + type Font = A::Font; + type Paragraph = A::Paragraph; + type Editor = A::Editor; + + const ICON_FONT: Self::Font = A::ICON_FONT; + const CHECKMARK_ICON: char = A::CHECKMARK_ICON; + const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON; + + fn default_font(&self) -> Self::Font { + delegate!(self, renderer, renderer.default_font()) + } + + fn default_size(&self) -> core::Pixels { + delegate!(self, renderer, renderer.default_size()) + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_paragraph(text, position, color, clip_bounds) + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_editor(editor, position, color, clip_bounds) + ); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + delegate!( + self, + renderer, + renderer.fill_text(text, position, color, clip_bounds) + ); + } +} + +impl image::Renderer for Renderer +where + A: image::Renderer, + B: image::Renderer, +{ + type Handle = A::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size { + delegate!(self, renderer, renderer.measure_image(handle)) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + rotation: Radians, + opacity: f32, + ) { + delegate!( + self, + renderer, + renderer.draw_image( + handle, + filter_method, + bounds, + rotation, + opacity + ) + ); + } +} + +impl svg::Renderer for Renderer +where + A: svg::Renderer, + B: svg::Renderer, +{ + fn measure_svg(&self, handle: &svg::Handle) -> Size { + delegate!(self, renderer, renderer.measure_svg(handle)) + } + + fn draw_svg( + &mut self, + handle: svg::Handle, + color: Option, + bounds: Rectangle, + rotation: Radians, + opacity: f32, + ) { + delegate!( + self, + renderer, + renderer.draw_svg(handle, color, bounds, rotation, opacity) + ); + } +} + +impl mesh::Renderer for Renderer +where + A: mesh::Renderer, + B: mesh::Renderer, +{ + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + delegate!(self, renderer, renderer.draw_mesh(mesh)); + } +} + +/// A compositor `A` with a fallback strategy `B`. +/// +/// It works analogously to [`Renderer`]. +#[derive(Debug)] +pub enum Compositor +where + A: graphics::Compositor, + B: graphics::Compositor, +{ + /// The primary compositing option. + Primary(A), + /// The secondary (or fallback) compositing option. + Secondary(B), +} + +/// A surface `A` with a fallback strategy `B`. +/// +/// It works analogously to [`Renderer`]. +#[derive(Debug)] +pub enum Surface { + /// The primary surface option. + Primary(A), + /// The secondary (or fallback) surface option. + Secondary(B), +} + +impl graphics::Compositor for Compositor +where + A: graphics::Compositor, + B: graphics::Compositor, +{ + type Renderer = Renderer; + type Surface = Surface; + + async fn with_backend( + settings: graphics::Settings, + compatible_window: W, + backend: Option<&str>, + ) -> Result { + use std::env; + + let backends = backend + .map(str::to_owned) + .or_else(|| env::var("ICED_BACKEND").ok()); + + let mut candidates: Vec<_> = backends + .map(|backends| { + backends + .split(',') + .filter(|candidate| !candidate.is_empty()) + .map(str::to_owned) + .map(Some) + .collect() + }) + .unwrap_or_default(); + + if candidates.is_empty() { + candidates.push(None); + } + + let mut errors = vec![]; + + for backend in candidates.iter().map(Option::as_deref) { + match A::with_backend(settings, compatible_window.clone(), backend) + .await + { + Ok(compositor) => return Ok(Self::Primary(compositor)), + Err(error) => { + errors.push(error); + } + } + + match B::with_backend(settings, compatible_window.clone(), backend) + .await + { + Ok(compositor) => return Ok(Self::Secondary(compositor)), + Err(error) => { + errors.push(error); + } + } + } + + Err(graphics::Error::List(errors)) + } + + fn create_renderer(&self) -> Self::Renderer { + match self { + Self::Primary(compositor) => { + Renderer::Primary(compositor.create_renderer()) + } + Self::Secondary(compositor) => { + Renderer::Secondary(compositor.create_renderer()) + } + } + } + + fn create_surface( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface { + match self { + Self::Primary(compositor) => Surface::Primary( + compositor.create_surface(window, width, height), + ), + Self::Secondary(compositor) => Surface::Secondary( + compositor.create_surface(window, width, height), + ), + } + } + + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ) { + match (self, surface) { + (Self::Primary(compositor), Surface::Primary(surface)) => { + compositor.configure_surface(surface, width, height); + } + (Self::Secondary(compositor), Surface::Secondary(surface)) => { + compositor.configure_surface(surface, width, height); + } + _ => unreachable!(), + } + } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + delegate!(self, compositor, compositor.load_font(font)); + } + + fn fetch_information(&self) -> compositor::Information { + delegate!(self, compositor, compositor.fetch_information()) + } + + fn present( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &graphics::Viewport, + background_color: Color, + ) -> Result<(), compositor::SurfaceError> { + match (self, renderer, surface) { + ( + Self::Primary(compositor), + Renderer::Primary(renderer), + Surface::Primary(surface), + ) => compositor.present( + renderer, + surface, + viewport, + background_color, + ), + ( + Self::Secondary(compositor), + Renderer::Secondary(renderer), + Surface::Secondary(surface), + ) => compositor.present( + renderer, + surface, + viewport, + background_color, + ), + _ => unreachable!(), + } + } + + fn screenshot( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &graphics::Viewport, + background_color: Color, + ) -> Vec { + match (self, renderer, surface) { + ( + Self::Primary(compositor), + Renderer::Primary(renderer), + Surface::Primary(surface), + ) => compositor.screenshot( + renderer, + surface, + viewport, + background_color, + ), + ( + Self::Secondary(compositor), + Renderer::Secondary(renderer), + Surface::Secondary(surface), + ) => compositor.screenshot( + renderer, + surface, + viewport, + background_color, + ), + _ => unreachable!(), + } + } +} + +#[cfg(feature = "wgpu")] +impl iced_wgpu::primitive::Renderer for Renderer +where + A: iced_wgpu::primitive::Renderer, + B: core::Renderer, +{ + fn draw_primitive( + &mut self, + bounds: Rectangle, + primitive: impl iced_wgpu::Primitive, + ) { + match self { + Self::Primary(renderer) => { + renderer.draw_primitive(bounds, primitive); + } + Self::Secondary(_) => { + log::warn!( + "Custom shader primitive is not supported with this renderer." + ); + } + } + } +} + +#[cfg(feature = "geometry")] +mod geometry { + use super::Renderer; + use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::graphics::cache::{self, Cached}; + use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; + + impl geometry::Renderer for Renderer + where + A: geometry::Renderer, + B: geometry::Renderer, + { + type Geometry = Geometry; + type Frame = Frame; + + fn new_frame(&self, size: iced_graphics::core::Size) -> Self::Frame { + match self { + Self::Primary(renderer) => { + Frame::Primary(renderer.new_frame(size)) + } + Self::Secondary(renderer) => { + Frame::Secondary(renderer.new_frame(size)) + } + } + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + match (self, geometry) { + (Self::Primary(renderer), Geometry::Primary(geometry)) => { + renderer.draw_geometry(geometry); + } + (Self::Secondary(renderer), Geometry::Secondary(geometry)) => { + renderer.draw_geometry(geometry); + } + _ => unreachable!(), + } + } + } + + #[derive(Debug, Clone)] + pub enum Geometry { + Primary(A), + Secondary(B), + } + + impl Cached for Geometry + where + A: Cached, + B: Cached, + { + type Cache = Geometry; + + fn load(cache: &Self::Cache) -> Self { + match cache { + Geometry::Primary(cache) => Self::Primary(A::load(cache)), + Geometry::Secondary(cache) => Self::Secondary(B::load(cache)), + } + } + + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { + match (self, previous) { + ( + Self::Primary(geometry), + Some(Geometry::Primary(previous)), + ) => Geometry::Primary(geometry.cache(group, Some(previous))), + (Self::Primary(geometry), None) => { + Geometry::Primary(geometry.cache(group, None)) + } + ( + Self::Secondary(geometry), + Some(Geometry::Secondary(previous)), + ) => Geometry::Secondary(geometry.cache(group, Some(previous))), + (Self::Secondary(geometry), None) => { + Geometry::Secondary(geometry.cache(group, None)) + } + _ => unreachable!(), + } + } + } + + #[derive(Debug)] + pub enum Frame { + Primary(A), + Secondary(B), + } + + impl geometry::frame::Backend for Frame + where + A: geometry::frame::Backend, + B: geometry::frame::Backend, + { + type Geometry = Geometry; + + fn width(&self) -> f32 { + delegate!(self, frame, frame.width()) + } + + fn height(&self) -> f32 { + delegate!(self, frame, frame.height()) + } + + fn size(&self) -> Size { + delegate!(self, frame, frame.size()) + } + + fn center(&self) -> Point { + delegate!(self, frame, frame.center()) + } + + fn fill(&mut self, path: &Path, fill: impl Into) { + delegate!(self, frame, frame.fill(path, fill)); + } + + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + delegate!(self, frame, frame.fill_rectangle(top_left, size, fill)); + } + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + delegate!(self, frame, frame.stroke(path, stroke)); + } + + fn fill_text(&mut self, text: impl Into) { + delegate!(self, frame, frame.fill_text(text)); + } + + fn push_transform(&mut self) { + delegate!(self, frame, frame.push_transform()); + } + + fn pop_transform(&mut self) { + delegate!(self, frame, frame.pop_transform()); + } + + fn draft(&mut self, bounds: Rectangle) -> Self { + match self { + Self::Primary(frame) => Self::Primary(frame.draft(bounds)), + Self::Secondary(frame) => Self::Secondary(frame.draft(bounds)), + } + } + + fn paste(&mut self, frame: Self, at: Point) { + match (self, frame) { + (Self::Primary(target), Self::Primary(source)) => { + target.paste(source, at); + } + (Self::Secondary(target), Self::Secondary(source)) => { + target.paste(source, at); + } + _ => unreachable!(), + } + } + + fn translate(&mut self, translation: Vector) { + delegate!(self, frame, frame.translate(translation)); + } + + fn rotate(&mut self, angle: impl Into) { + delegate!(self, frame, frame.rotate(angle)); + } + + fn scale(&mut self, scale: impl Into) { + delegate!(self, frame, frame.scale(scale)); + } + + fn scale_nonuniform(&mut self, scale: impl Into) { + delegate!(self, frame, frame.scale_nonuniform(scale)); + } + + fn into_geometry(self) -> Self::Geometry { + match self { + Frame::Primary(frame) => { + Geometry::Primary(frame.into_geometry()) + } + Frame::Secondary(frame) => { + Geometry::Secondary(frame.into_geometry()) + } + } + } + } +} + +impl compositor::Default for Renderer +where + A: compositor::Default, + B: compositor::Default, +{ + type Compositor = Compositor; +} diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs deleted file mode 100644 index f09ccfbf..00000000 --- a/renderer/src/geometry.rs +++ /dev/null @@ -1,210 +0,0 @@ -mod cache; - -pub use cache::Cache; - -use crate::core::{Point, Rectangle, Size, Transformation, Vector}; -use crate::graphics::geometry::{Fill, Path, Stroke, Text}; -use crate::Renderer; - -macro_rules! delegate { - ($frame:expr, $name:ident, $body:expr) => { - match $frame { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; -} - -pub enum Geometry { - TinySkia(iced_tiny_skia::Primitive), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Primitive), -} - -impl Geometry { - pub fn transform(self, transformation: Transformation) -> Self { - match self { - Self::TinySkia(primitive) => { - Self::TinySkia(primitive.transform(transformation)) - } - #[cfg(feature = "wgpu")] - Self::Wgpu(primitive) => { - Self::Wgpu(primitive.transform(transformation)) - } - } - } -} - -pub enum Frame { - TinySkia(iced_tiny_skia::geometry::Frame), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::geometry::Frame), -} - -impl Frame { - pub fn new(renderer: &Renderer, size: Size) -> Self { - match renderer { - Renderer::TinySkia(_) => { - Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) - } - #[cfg(feature = "wgpu")] - Renderer::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) - } - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - delegate!(self, frame, frame.width()) - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - delegate!(self, frame, frame.height()) - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - delegate!(self, frame, frame.size()) - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - delegate!(self, frame, frame.center()) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - delegate!(self, frame, frame.fill(path, fill)); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - delegate!(self, frame, frame.fill_rectangle(top_left, size, fill)); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - delegate!(self, frame, frame.stroke(path, stroke)); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a `Canvas`. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - pub fn fill_text(&mut self, text: impl Into) { - delegate!(self, frame, frame.fill_text(text)); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R { - delegate!(self, frame, frame.push_transform()); - - let result = f(self); - - delegate!(self, frame, frame.pop_transform()); - - result - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip( - &mut self, - region: Rectangle, - f: impl FnOnce(&mut Frame) -> R, - ) -> R { - let mut frame = match self { - Self::TinySkia(_) => Self::TinySkia( - iced_tiny_skia::geometry::Frame::new(region.size()), - ), - #[cfg(feature = "wgpu")] - Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size())) - } - }; - - let result = f(&mut frame); - - let origin = Point::new(region.x, region.y); - - match (self, frame) { - (Self::TinySkia(target), Self::TinySkia(frame)) => { - target.clip(frame, origin); - } - #[cfg(feature = "wgpu")] - (Self::Wgpu(target), Self::Wgpu(frame)) => { - target.clip(frame, origin); - } - #[allow(unreachable_patterns)] - _ => unreachable!(), - }; - - result - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - delegate!(self, frame, frame.translate(translation)); - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - delegate!(self, frame, frame.rotate(angle)); - } - - /// Applies a uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: impl Into) { - delegate!(self, frame, frame.scale(scale)); - } - - /// Applies a non-uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale_nonuniform(&mut self, scale: impl Into) { - delegate!(self, frame, frame.scale_nonuniform(scale)); - } - - pub fn into_geometry(self) -> Geometry { - match self { - Self::TinySkia(frame) => Geometry::TinySkia(frame.into_primitive()), - #[cfg(feature = "wgpu")] - Self::Wgpu(frame) => Geometry::Wgpu(frame.into_primitive()), - } - } -} diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs deleted file mode 100644 index 3aff76b9..00000000 --- a/renderer/src/geometry/cache.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::core::Size; -use crate::geometry::{Frame, Geometry}; -use crate::Renderer; - -use std::cell::RefCell; -use std::sync::Arc; - -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -#[derive(Debug, Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Internal, - }, -} - -#[derive(Debug, Clone)] -enum Internal { - TinySkia(Arc), - #[cfg(feature = "wgpu")] - Wgpu(Arc), -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: RefCell::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - renderer: &Renderer, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - match primitive { - Internal::TinySkia(primitive) => { - return Geometry::TinySkia( - iced_tiny_skia::Primitive::Cache { - content: primitive.clone(), - }, - ); - } - #[cfg(feature = "wgpu")] - Internal::Wgpu(primitive) => { - return Geometry::Wgpu(iced_wgpu::Primitive::Cache { - content: primitive.clone(), - }); - } - } - } - } - - let mut frame = Frame::new(renderer, bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - match geometry { - Geometry::TinySkia(primitive) => { - Internal::TinySkia(Arc::new(primitive)) - } - #[cfg(feature = "wgpu")] - Geometry::Wgpu(primitive) => { - Internal::Wgpu(Arc::new(primitive)) - } - } - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - match primitive { - Internal::TinySkia(primitive) => { - Geometry::TinySkia(iced_tiny_skia::Primitive::Cache { - content: primitive, - }) - } - #[cfg(feature = "wgpu")] - Internal::Wgpu(primitive) => { - Geometry::Wgpu(iced_wgpu::Primitive::Cache { - content: primitive, - }) - } - } - } -} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 757c264d..056da5ed 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,302 +1,53 @@ -#![forbid(rust_2018_idioms)] -#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)] +//! The official renderer for iced. #![cfg_attr(docsrs, feature(doc_auto_cfg))] #[cfg(feature = "wgpu")] pub use iced_wgpu as wgpu; -pub mod compositor; - -#[cfg(feature = "geometry")] -pub mod geometry; - -mod settings; +pub mod fallback; pub use iced_graphics as graphics; pub use iced_graphics::core; -pub use compositor::Compositor; -pub use settings::Settings; - #[cfg(feature = "geometry")] -pub use geometry::Geometry; - -use crate::core::renderer; -use crate::core::text::{self, Text}; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, -}; -use crate::graphics::text::Editor; -use crate::graphics::text::Paragraph; -use crate::graphics::Mesh; - -use std::borrow::Cow; +pub use iced_graphics::geometry; /// The default graphics renderer for [`iced`]. /// /// [`iced`]: https://github.com/iced-rs/iced -pub enum Renderer { - TinySkia(iced_tiny_skia::Renderer), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Renderer), +pub type Renderer = renderer::Renderer; + +/// The default graphics compositor for [`iced`]. +/// +/// [`iced`]: https://github.com/iced-rs/iced +pub type Compositor = renderer::Compositor; + +#[cfg(all(feature = "wgpu", feature = "tiny-skia"))] +mod renderer { + pub type Renderer = crate::fallback::Renderer< + iced_wgpu::Renderer, + iced_tiny_skia::Renderer, + >; + + pub type Compositor = crate::fallback::Compositor< + iced_wgpu::window::Compositor, + iced_tiny_skia::window::Compositor, + >; } -macro_rules! delegate { - ($renderer:expr, $name:ident, $body:expr) => { - match $renderer { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; +#[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))] +mod renderer { + pub type Renderer = iced_wgpu::Renderer; + pub type Compositor = iced_wgpu::window::Compositor; } -impl Renderer { - pub fn draw_mesh(&mut self, mesh: Mesh) { - match self { - Self::TinySkia(_) => { - log::warn!("Unsupported mesh primitive: {mesh:?}"); - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.draw_primitive(iced_wgpu::Primitive::Custom( - iced_wgpu::primitive::Custom::Mesh(mesh), - )); - } - } - } +#[cfg(all(not(feature = "wgpu"), feature = "tiny-skia"))] +mod renderer { + pub type Renderer = iced_tiny_skia::Renderer; + pub type Compositor = iced_tiny_skia::window::Compositor; } -impl core::Renderer for Renderer { - fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - match self { - Self::TinySkia(renderer) => { - let primitives = renderer.start_layer(); - - f(self); - - match self { - Self::TinySkia(renderer) => { - renderer.end_layer(primitives, bounds); - } - #[cfg(feature = "wgpu")] - _ => unreachable!(), - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - let primitives = renderer.start_layer(); - - f(self); - - match self { - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.end_layer(primitives, bounds); - } - _ => unreachable!(), - } - } - } - } - - fn with_transformation( - &mut self, - transformation: Transformation, - f: impl FnOnce(&mut Self), - ) { - match self { - Self::TinySkia(renderer) => { - let primitives = renderer.start_transformation(); - - f(self); - - match self { - Self::TinySkia(renderer) => { - renderer.end_transformation(primitives, transformation); - } - #[cfg(feature = "wgpu")] - _ => unreachable!(), - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - let primitives = renderer.start_transformation(); - - f(self); - - match self { - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - renderer.end_transformation(primitives, transformation); - } - _ => unreachable!(), - } - } - } - } - - fn fill_quad( - &mut self, - quad: renderer::Quad, - background: impl Into, - ) { - delegate!(self, renderer, renderer.fill_quad(quad, background)); - } - - fn clear(&mut self) { - delegate!(self, renderer, renderer.clear()); - } -} - -impl text::Renderer for Renderer { - type Font = Font; - type Paragraph = Paragraph; - type Editor = Editor; - - const ICON_FONT: Font = iced_tiny_skia::Renderer::ICON_FONT; - const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::CHECKMARK_ICON; - const ARROW_DOWN_ICON: char = iced_tiny_skia::Renderer::ARROW_DOWN_ICON; - - fn default_font(&self) -> Self::Font { - delegate!(self, renderer, renderer.default_font()) - } - - fn default_size(&self) -> Pixels { - delegate!(self, renderer, renderer.default_size()) - } - - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - delegate!(self, renderer, renderer.load_font(bytes)); - } - - fn fill_paragraph( - &mut self, - paragraph: &Self::Paragraph, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_paragraph(paragraph, position, color, clip_bounds) - ); - } - - fn fill_editor( - &mut self, - editor: &Self::Editor, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_editor(editor, position, color, clip_bounds) - ); - } - - fn fill_text( - &mut self, - text: Text<'_, Self::Font>, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - delegate!( - self, - renderer, - renderer.fill_text(text, position, color, clip_bounds) - ); - } -} - -#[cfg(feature = "image")] -impl crate::core::image::Renderer for Renderer { - type Handle = crate::core::image::Handle; - - fn dimensions( - &self, - handle: &crate::core::image::Handle, - ) -> core::Size { - delegate!(self, renderer, renderer.dimensions(handle)) - } - - fn draw( - &mut self, - handle: crate::core::image::Handle, - filter_method: crate::core::image::FilterMethod, - bounds: Rectangle, - ) { - delegate!(self, renderer, renderer.draw(handle, filter_method, bounds)); - } -} - -#[cfg(feature = "svg")] -impl crate::core::svg::Renderer for Renderer { - fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size { - delegate!(self, renderer, renderer.dimensions(handle)) - } - - fn draw( - &mut self, - handle: crate::core::svg::Handle, - color: Option, - bounds: Rectangle, - ) { - delegate!(self, renderer, renderer.draw(handle, color, bounds)); - } -} - -#[cfg(feature = "geometry")] -impl crate::graphics::geometry::Renderer for Renderer { - type Geometry = crate::Geometry; - - fn draw(&mut self, layers: Vec) { - match self { - Self::TinySkia(renderer) => { - for layer in layers { - match layer { - crate::Geometry::TinySkia(primitive) => { - renderer.draw_primitive(primitive); - } - #[cfg(feature = "wgpu")] - crate::Geometry::Wgpu(_) => unreachable!(), - } - } - } - #[cfg(feature = "wgpu")] - Self::Wgpu(renderer) => { - for layer in layers { - match layer { - crate::Geometry::Wgpu(primitive) => { - renderer.draw_primitive(primitive); - } - crate::Geometry::TinySkia(_) => unreachable!(), - } - } - } - } - } -} - -#[cfg(feature = "wgpu")] -impl iced_wgpu::primitive::pipeline::Renderer for Renderer { - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl wgpu::primitive::pipeline::Primitive, - ) { - match self { - Self::TinySkia(_renderer) => { - log::warn!( - "Custom shader primitive is unavailable with tiny-skia." - ); - } - Self::Wgpu(renderer) => { - renderer.draw_pipeline_primitive(bounds, primitive); - } - } - } +#[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] +mod renderer { + pub type Renderer = (); + pub type Compositor = (); } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6d49ef93..dfe3a1b0 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -10,10 +10,14 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [features] multi-window = [] [dependencies] +bytes.workspace = true iced_core.workspace = true iced_debug.workspace = true diff --git a/runtime/src/command.rs b/runtime/src/command.rs index f70da915..f7a746fe 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 18059ef5..bea8e9a0 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -8,13 +8,6 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; pub mod command; diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 2150de76..21a4d9a9 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -43,7 +43,7 @@ where caches, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/runtime/src/program.rs b/runtime/src/program.rs index 6c1b8f07..0ea94d3b 100644 --- a/runtime/src/program.rs +++ b/runtime/src/program.rs @@ -2,7 +2,7 @@ use crate::Command; use iced_core::text; -use iced_core::{Element, Renderer}; +use iced_core::Element; mod state; @@ -11,7 +11,7 @@ pub use state::State; /// The core of a user interface application following The Elm Architecture. pub trait Program: Sized { /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer + text::Renderer; + type Renderer: text::Renderer; /// The theme used to draw the [`Program`]. type Theme; diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index cec3186b..182169be 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -47,7 +47,7 @@ where cache, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 748fb651..006225ed 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -45,7 +45,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # } /// # /// # pub struct Counter; @@ -62,7 +62,7 @@ where /// // Initialization /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// /// // Application loop @@ -121,7 +121,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # } /// # /// # pub struct Counter; @@ -139,7 +139,7 @@ where /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; @@ -374,7 +374,7 @@ where /// /// ```no_run /// # mod iced_wgpu { - /// # pub use iced_runtime::core::renderer::Null as Renderer; + /// # pub type Renderer = (); /// # pub type Theme = (); /// # } /// # @@ -394,7 +394,7 @@ where /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); - /// let mut renderer = Renderer::new(); + /// let mut renderer = Renderer::default(); /// let mut window_size = Size::new(1024.0, 768.0); /// let mut cursor = mouse::Cursor::default(); /// let mut clipboard = clipboard::Null; diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 24171e3e..e32465d3 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -197,7 +197,7 @@ pub fn change_icon(id: Id, icon: Icon) -> Command { /// Note that if the window closes before this call is processed the callback will not be run. pub fn run_with_handle( id: Id, - f: impl FnOnce(&WindowHandle<'_>) -> Message + 'static, + f: impl FnOnce(WindowHandle<'_>) -> Message + 'static, ) -> Command { Command::single(command::Action::Window(Action::RunWithHandle( id, diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index e44ff5a6..07e77872 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -106,7 +106,7 @@ pub enum Action { /// said, it's usually in the same ballpark as on Windows. ChangeIcon(Id, Icon), /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box) -> T + 'static>), + RunWithHandle(Id, Box) -> T + 'static>), /// Screenshot the viewport of the window. Screenshot(Id, Box T + 'static>), } diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index 21e04718..fb318110 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -1,8 +1,8 @@ //! Take screenshots of a window. use crate::core::{Rectangle, Size}; +use bytes::Bytes; use std::fmt::{Debug, Formatter}; -use std::sync::Arc; /// Data of a screenshot, captured with `window::screenshot()`. /// @@ -10,7 +10,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct Screenshot { /// The bytes of the [`Screenshot`]. - pub bytes: Arc>, + pub bytes: Bytes, /// The size of the [`Screenshot`]. pub size: Size, } @@ -28,9 +28,9 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. - pub fn new(bytes: Vec, size: Size) -> Self { + pub fn new(bytes: impl Into, size: Size) -> Self { Self { - bytes: Arc::new(bytes), + bytes: bytes.into(), size, } } @@ -68,7 +68,7 @@ impl Screenshot { ); Ok(Self { - bytes: Arc::new(chopped), + bytes: Bytes::from(chopped), size: Size::new(region.width, region.height), }) } @@ -80,6 +80,12 @@ impl AsRef<[u8]> for Screenshot { } } +impl From for Bytes { + fn from(screenshot: Screenshot) -> Self { + screenshot.bytes + } +} + #[derive(Debug, thiserror::Error)] /// Errors that can occur when cropping a [`Screenshot`]. pub enum CropError { diff --git a/sentinel/Cargo.toml b/sentinel/Cargo.toml index 0b22fbcd..d8ec8e64 100644 --- a/sentinel/Cargo.toml +++ b/sentinel/Cargo.toml @@ -12,9 +12,7 @@ keywords.workspace = true [dependencies] iced_core.workspace = true - -iced_style.workspace = true -iced_style.features = ["serde"] +iced_core.features = ["serde"] bincode.workspace = true futures.workspace = true diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index 3a7b38c8..79e3dca4 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -1,5 +1,5 @@ use crate::core::time::SystemTime; -use crate::style::theme; +use crate::theme; use crate::{Input, Timing, SOCKET_ADDRESS}; use tokio::io::{self, AsyncWriteExt}; diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index 49b2f1b9..e7377861 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -1,12 +1,11 @@ pub use iced_core as core; -pub use iced_style as style; pub use semver::Version; pub mod client; pub mod timing; +use crate::core::theme; use crate::core::time::SystemTime; -use crate::style::theme; use crate::timing::Timing; use futures::future; diff --git a/src/advanced.rs b/src/advanced.rs index 52fb3f2b..ed768b51 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,4 +1,5 @@ //! 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}; @@ -8,11 +9,13 @@ 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::{Hasher, Shell}; +pub use crate::core::Shell; pub use crate::renderer::graphics; pub use iced_debug as debug; pub mod subscription { //! Write your own subscriptions. - pub use crate::runtime::futures::subscription::{EventStream, Recipe}; + pub use crate::runtime::futures::subscription::{ + EventStream, Hasher, Recipe, + }; } diff --git a/src/application.rs b/src/application.rs index 3d89c758..d12ba73d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,7 +1,10 @@ //! Build interactive cross-platform applications. +use crate::core::text; +use crate::graphics::compositor; +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 +16,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`. @@ -59,8 +60,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, Renderer}; /// /// pub fn main() -> iced::Result { /// Hello::run(Settings::default()) @@ -73,6 +75,7 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// type Flags = (); /// type Message = (); /// type Theme = Theme; +/// type Renderer = Renderer; /// /// fn new(_flags: ()) -> (Hello, Command) { /// (Hello, Command::none()) @@ -91,7 +94,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 +110,10 @@ pub trait Application: Sized { type Message: std::fmt::Debug + Send; /// The theme of your [`Application`]. - type Theme: Default + StyleSheet; + type Theme: Default; + + /// The renderer of your [`Application`]. + type Renderer: text::Renderer + compositor::Default; /// The data needed to initialize your [`Application`]. type Flags; @@ -139,7 +148,7 @@ pub trait Application: Sized { /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; + fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; /// Returns the current [`Theme`] of the [`Application`]. /// @@ -148,11 +157,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 @@ -194,7 +201,7 @@ pub trait Application: Sized { Self: 'static, { #[allow(clippy::needless_update)] - let renderer_settings = crate::renderer::Settings { + let renderer_settings = crate::graphics::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { @@ -202,26 +209,30 @@ pub trait Application: Sized { } else { None }, - ..crate::renderer::Settings::default() + ..crate::graphics::Settings::default() }; Ok(crate::shell::application::run::< Instance, Self::Executor, - crate::renderer::Compositor, + ::Compositor, >(settings.into(), renderer_settings)?) } } -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; - type Renderer = crate::Renderer; + type Renderer = A::Renderer; fn update(&mut self, message: Self::Message) -> Command { self.0.update(message) @@ -232,9 +243,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 +264,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 236c64d3..50ee7ecc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ //! We start by modelling the __state__ of our application: //! //! ``` +//! #[derive(Default)] //! struct Counter { //! // The counter value //! value: i32, @@ -63,8 +64,8 @@ //! ``` //! #[derive(Debug, Clone, Copy)] //! pub enum Message { -//! IncrementPressed, -//! DecrementPressed, +//! Increment, +//! Decrement, //! } //! ``` //! @@ -79,8 +80,8 @@ //! # //! # #[derive(Debug, Clone, Copy)] //! # pub enum Message { -//! # IncrementPressed, -//! # DecrementPressed, +//! # Increment, +//! # Decrement, //! # } //! # //! use iced::widget::{button, column, text, Column}; @@ -90,15 +91,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 +116,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 +135,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,26 +158,18 @@ //! 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" )] -#![forbid(rust_2018_idioms, unsafe_code)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![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 +179,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,16 +193,15 @@ 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, - Transformation, Vector, + Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, + Theme, Transformation, Vector, }; pub mod clipboard { @@ -304,17 +310,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. /// @@ -326,7 +330,55 @@ 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, Renderer> + 'static, +) -> Result +where + State: Default + 'static, + Message: std::fmt::Debug + Send + 'static, + Theme: Default + program::DefaultStyle + 'static, + Renderer: program::Renderer + '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 5b7a00b4..b81297dc 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::graphics::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::graphics::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 ac625281..00000000 --- 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 00000000..d4c2a266 --- /dev/null +++ b/src/program.rs @@ -0,0 +1,879 @@ +//! 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::core::text; +use crate::executor::{self, Executor}; +use crate::graphics::compositor; +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, Renderer>( + title: impl Title<State>, + update: impl Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> +where + State: 'static, + Message: Send + std::fmt::Debug, + Theme: Default + DefaultStyle, + Renderer: self::Renderer, +{ + use std::marker::PhantomData; + + struct Application<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, + } + + impl<State, Message, Theme, Renderer, Update, View> Definition + for Application<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug, + Theme: Default + DefaultStyle, + Renderer: self::Renderer, + Update: self::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + 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::Renderer> { + self.view.view(state).into() + } + } + + Program { + raw: Application { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: 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 Renderer = P::Renderer; + 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, Self::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 renderer of the program. + type Renderer: Renderer; + + /// 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, Self::Renderer>; + + 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 Renderer = P::Renderer; + 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::Renderer> { + 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 Renderer = P::Renderer; + 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::Renderer> { + 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 Renderer = P::Renderer; + 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::Renderer> { + 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 Renderer = P::Renderer; + 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::Renderer> { + 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 Renderer = P::Renderer; + 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::Renderer> { + 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, Renderer> { + /// Produces the widget of the [`Program`]. + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state) + } +} + +/// The renderer of some [`Program`]. +pub trait Renderer: text::Renderer + compositor::Default {} + +impl<T> Renderer for T where T: text::Renderer + compositor::Default {} diff --git a/src/sandbox.rs b/src/sandbox.rs deleted file mode 100644 index 28461929..00000000 --- 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.12/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool -/// [`counter`]: https://github.com/iced-rs/iced/tree/0.12/examples/counter -/// [`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 -/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.12/examples/pane_grid -/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.12/examples/progress_bar -/// [`styling`]: https://github.com/iced-rs/iced/tree/0.12/examples/styling -/// [`svg`]: https://github.com/iced-rs/iced/tree/0.12/examples/svg -/// [`tour`]: https://github.com/iced-rs/iced/tree/0.12/examples/tour -/// [`Canvas widget`]: crate::widget::Canvas -/// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.12/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 d9476b61..f7947841 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 38aa54ab..55982277 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, SystemTime}; +pub use crate::core::time::{Duration, Instant, SystemTime}; #[allow(unused_imports)] #[cfg_attr( diff --git a/src/window/icon.rs b/src/window/icon.rs index ef71c228..7fe4ca7b 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -54,7 +54,7 @@ pub enum Error { InvalidError(#[from] icon::Error), /// The underlying OS failed to create the icon. - #[error("The underlying OS failted to create the window icon: {0}")] + #[error("The underlying OS failed to create the window icon: {0}")] OsError(#[from] io::Error), /// The `image` crate reported an error. diff --git a/style/Cargo.toml b/style/Cargo.toml deleted file mode 100644 index 9f28c670..00000000 --- a/style/Cargo.toml +++ /dev/null @@ -1,25 +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 - -[features] -serde = ["dep:serde", "iced_core/serde"] - -[dependencies] -iced_core.workspace = true -iced_core.features = ["palette"] - -palette.workspace = true -once_cell.workspace = true - -serde.workspace = true -serde.optional = true -serde.features = ["derive"] diff --git a/style/src/application.rs b/style/src/application.rs deleted file mode 100644 index db9a673a..00000000 --- a/style/src/application.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Change the appearance of an application. -use crate::core::Color; -use crate::theme; - -/// 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; - - /// Returns the [`theme::Palette`] of the application, if any. - /// - /// This may be used by other parts of the `iced` runtime to - /// try to match the style of your application. - /// - /// For instance, the Iced Axe uses this [`theme::Palette`] to - /// automatically style itself using your application's colors. - fn palette(&self) -> Option<theme::Palette> { - None - } -} - -/// 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 0d7a668a..00000000 --- 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 77093f69..00000000 --- a/style/src/checkbox.rs +++ /dev/null @@ -1,45 +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 { - let active = self.active(style, is_checked); - - Appearance { - background: match active.background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => { - Background::Gradient(gradient.mul_alpha(0.5)) - } - }, - ..active - } - } -} diff --git a/style/src/container.rs b/style/src/container.rs deleted file mode 100644 index 00649c25..00000000 --- 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 67fcad52..00000000 --- a/style/src/lib.rs +++ /dev/null @@ -1,39 +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)] -#![deny( - unused_results, - missing_docs, - unused_results, - rust_2018_idioms, - 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 be60a3f8..00000000 --- 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 35570584..00000000 --- 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 8f008f4a..00000000 --- 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 b62512d8..00000000 --- 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 02c4709a..00000000 --- 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 06c49029..00000000 --- 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 12980da7..00000000 --- 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 d2348510..00000000 --- a/style/src/scrollable.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Change the appearance of a scrollable. -use crate::container; -use crate::core::{Background, Border, Color}; - -/// The appearance of a scrolable. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`container::Appearance`] of a scrollable. - pub container: container::Appearance, - /// The [`Scrollbar`] appearance. - pub 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, -} - -/// 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 [`Appearance`] of an active scrollable. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the [`Appearance`] of a scrollable when it is being hovered. - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> Appearance; - - /// Produces the [`Appearance`] of a scrollable when it is being dragged. - fn dragging(&self, style: &Self::Style) -> Appearance { - self.hovered(style, true) - } -} diff --git a/style/src/slider.rs b/style/src/slider.rs deleted file mode 100644 index bf1c7329..00000000 --- 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 3fe5546b..00000000 --- a/style/src/svg.rs +++ /dev/null @@ -1,28 +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 { - self.appearance(style) - } -} diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs deleted file mode 100644 index 87f481e3..00000000 --- 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 8ba9957f..00000000 --- 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 6844039d..00000000 --- a/style/src/theme.rs +++ /dev/null @@ -1,1527 +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), - } - } - - fn palette(&self) -> Option<Palette> { - Some(self.palette()) - } -} - -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::Appearance { - match style { - Scrollable::Default => { - let palette = self.extended_palette(); - - scrollable::Appearance { - container: container::Appearance::default(), - scrollbar: 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), - }, - }, - gap: None, - } - } - Scrollable::Custom(custom) => custom.active(self), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> scrollable::Appearance { - match style { - Scrollable::Default => { - if is_mouse_over_scrollbar { - let palette = self.extended_palette(); - - scrollable::Appearance { - scrollbar: 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), - }, - }, - ..self.active(style) - } - } else { - self.active(style) - } - } - Scrollable::Custom(custom) => { - custom.hovered(self, is_mouse_over_scrollbar) - } - } - } - - fn dragging(&self, style: &Self::Style) -> scrollable::Appearance { - match style { - Scrollable::Default => self.hovered(style, true), - Scrollable::Custom(custom) => custom.dragging(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 731e87ce..00000000 --- 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/Cargo.toml b/tiny_skia/Cargo.toml index 68b2a03a..32ead3e0 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [features] image = ["iced_graphics/image"] svg = ["resvg"] @@ -25,7 +28,6 @@ log.workspace = true rustc-hash.workspace = true softbuffer.workspace = true tiny-skia.workspace = true -xxhash-rust.workspace = true resvg.workspace = true resvg.optional = true diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs deleted file mode 100644 index d2811fe2..00000000 --- a/tiny_skia/src/backend.rs +++ /dev/null @@ -1,972 +0,0 @@ -use crate::core::{ - Background, Color, Gradient, Rectangle, Size, Transformation, Vector, -}; -use crate::graphics::backend; -use crate::graphics::text; -use crate::graphics::{Damage, Viewport}; -use crate::primitive::{self, Primitive}; - -use std::borrow::Cow; - -pub struct Backend { - text_pipeline: crate::text::Pipeline, - - #[cfg(feature = "image")] - raster_pipeline: crate::raster::Pipeline, - - #[cfg(feature = "svg")] - vector_pipeline: crate::vector::Pipeline, -} - -impl Backend { - pub fn new() -> Self { - Self { - text_pipeline: crate::text::Pipeline::new(), - - #[cfg(feature = "image")] - raster_pipeline: crate::raster::Pipeline::new(), - - #[cfg(feature = "svg")] - vector_pipeline: crate::vector::Pipeline::new(), - } - } - - pub fn draw( - &mut self, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - primitives: &[Primitive], - viewport: &Viewport, - damage: &[Rectangle], - background_color: Color, - ) { - let scale_factor = viewport.scale_factor() as f32; - - for ®ion in damage { - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - region.x, - region.y, - region.width, - region.height, - ) - .expect("Create damage rectangle"), - ); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - background_color, - )), - anti_alias: false, - blend_mode: tiny_skia::BlendMode::Source, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - - adjust_clip_mask(clip_mask, region); - - for primitive in primitives { - self.draw_primitive( - primitive, - pixels, - clip_mask, - region, - scale_factor, - Transformation::IDENTITY, - ); - } - } - - self.text_pipeline.trim_cache(); - - #[cfg(feature = "image")] - self.raster_pipeline.trim_cache(); - - #[cfg(feature = "svg")] - self.vector_pipeline.trim_cache(); - } - - fn draw_primitive( - &mut self, - primitive: &Primitive, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, - scale_factor: f32, - transformation: Transformation, - ) { - match primitive { - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - debug_assert!( - bounds.width.is_normal(), - "Quad with non-normal width!" - ); - debug_assert!( - bounds.height.is_normal(), - "Quad with non-normal height!" - ); - - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - let transform = into_transform(transformation) - .post_scale(scale_factor, scale_factor); - - // Make sure the border radius is not larger than the bounds - let border_width = border - .width - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); - - let mut fill_border_radius = <[f32; 4]>::from(border.radius); - for radius in &mut fill_border_radius { - *radius = (*radius) - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); - } - let path = rounded_rectangle(*bounds, fill_border_radius); - - if shadow.color.a > 0.0 { - let shadow_bounds = (Rectangle { - x: bounds.x + shadow.offset.x - shadow.blur_radius, - y: bounds.y + shadow.offset.y - shadow.blur_radius, - width: bounds.width + shadow.blur_radius * 2.0, - height: bounds.height + shadow.blur_radius * 2.0, - } * transformation) - * scale_factor; - - let radii = fill_border_radius - .into_iter() - .map(|radius| radius * scale_factor) - .collect::<Vec<_>>(); - let (x, y, width, height) = ( - shadow_bounds.x as u32, - shadow_bounds.y as u32, - shadow_bounds.width as u32, - shadow_bounds.height as u32, - ); - let half_width = physical_bounds.width / 2.0; - let half_height = physical_bounds.height / 2.0; - - let colors = (y..y + height) - .flat_map(|y| { - (x..x + width).map(move |x| (x as f32, y as f32)) - }) - .filter_map(|(x, y)| { - tiny_skia::Size::from_wh(half_width, half_height) - .map(|size| { - let shadow_distance = rounded_box_sdf( - Vector::new( - x - physical_bounds.position().x - - (shadow.offset.x - * scale_factor) - - half_width, - y - physical_bounds.position().y - - (shadow.offset.y - * scale_factor) - - half_height, - ), - size, - &radii, - ); - let shadow_alpha = 1.0 - - smoothstep( - -shadow.blur_radius * scale_factor, - shadow.blur_radius * scale_factor, - shadow_distance, - ); - - let mut color = into_color(shadow.color); - color.apply_opacity(shadow_alpha); - - color.to_color_u8().premultiply() - }) - }) - .collect(); - - if let Some(pixmap) = tiny_skia::IntSize::from_wh( - width, height, - ) - .and_then(|size| { - tiny_skia::Pixmap::from_vec( - bytemuck::cast_vec(colors), - size, - ) - }) { - pixels.draw_pixmap( - x as i32, - y as i32, - pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::default(), - None, - ); - } - } - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: match background { - Background::Color(color) => { - tiny_skia::Shader::SolidColor(into_color( - *color, - )) - } - Background::Gradient(Gradient::Linear(linear)) => { - let (start, end) = - linear.angle.to_distance(bounds); - - let stops: Vec<tiny_skia::GradientStop> = - linear - .stops - .into_iter() - .flatten() - .map(|stop| { - tiny_skia::GradientStop::new( - stop.offset, - tiny_skia::Color::from_rgba( - stop.color.b, - stop.color.g, - stop.color.r, - stop.color.a, - ) - .expect("Create color"), - ) - }) - .collect(); - - tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: start.x, - y: start.y, - }, - tiny_skia::Point { x: end.x, y: end.y }, - if stops.is_empty() { - vec![tiny_skia::GradientStop::new( - 0.0, - tiny_skia::Color::BLACK, - )] - } else { - stops - }, - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient") - } - }, - anti_alias: true, - ..tiny_skia::Paint::default() - }, - tiny_skia::FillRule::EvenOdd, - transform, - clip_mask, - ); - - if border_width > 0.0 { - // Border path is offset by half the border width - let border_bounds = Rectangle { - x: bounds.x + border_width / 2.0, - y: bounds.y + border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, - }; - - // Make sure the border radius is correct - let mut border_radius = <[f32; 4]>::from(border.radius); - let mut is_simple_border = true; - - for radius in &mut border_radius { - *radius = if *radius == 0.0 { - // Path should handle this fine - 0.0 - } else if *radius > border_width / 2.0 { - *radius - border_width / 2.0 - } else { - is_simple_border = false; - 0.0 - } - .min(border_bounds.width / 2.0) - .min(border_bounds.height / 2.0); - } - - // Stroking a path works well in this case - if is_simple_border { - let border_path = - rounded_rectangle(border_bounds, border_radius); - - pixels.stroke_path( - &border_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor( - into_color(border.color), - ), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - clip_mask, - ); - } else { - // Draw corners that have too small border radii as having no border radius, - // but mask them with the rounded rectangle with the correct border radius. - let mut temp_pixmap = tiny_skia::Pixmap::new( - bounds.width as u32, - bounds.height as u32, - ) - .unwrap(); - - let mut quad_mask = tiny_skia::Mask::new( - bounds.width as u32, - bounds.height as u32, - ) - .unwrap(); - - let zero_bounds = Rectangle { - x: 0.0, - y: 0.0, - width: bounds.width, - height: bounds.height, - }; - let path = - rounded_rectangle(zero_bounds, fill_border_radius); - - quad_mask.fill_path( - &path, - tiny_skia::FillRule::EvenOdd, - true, - transform, - ); - let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, - }; - - let border_radius_path = - rounded_rectangle(path_bounds, border_radius); - - temp_pixmap.stroke_path( - &border_radius_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor( - into_color(border.color), - ), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - Some(&quad_mask), - ); - - pixels.draw_pixmap( - bounds.x as i32, - bounds.y as i32, - temp_pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - transform, - clip_mask, - ); - } - } - } - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = - Rectangle::new(*position, paragraph.min_bounds) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_paragraph( - paragraph, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = Rectangle::new(*position, editor.bounds) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_editor( - editor, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::Text { - content, - bounds, - color, - size, - line_height, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = - primitive.bounds() * transformation * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_cached( - content, - *bounds, - *color, - *size, - *line_height, - *font, - *horizontal_alignment, - *vertical_alignment, - *shaping, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::RawText(text::Raw { - buffer, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - }) => { - let Some(buffer) = buffer.upgrade() else { - return; - }; - - let (width, height) = buffer.size(); - - let physical_bounds = - Rectangle::new(*position, Size::new(width, height)) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_raw( - &buffer, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - #[cfg(feature = "image")] - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - let transform = into_transform(transformation) - .post_scale(scale_factor, scale_factor); - - self.raster_pipeline.draw( - handle, - *filter_method, - *bounds, - pixels, - transform, - clip_mask, - ); - } - #[cfg(not(feature = "image"))] - Primitive::Image { .. } => { - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {primitive:?}", - ); - } - #[cfg(feature = "svg")] - Primitive::Svg { - handle, - bounds, - color, - } => { - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.vector_pipeline.draw( - handle, - *color, - (*bounds * transformation) * scale_factor, - pixels, - clip_mask, - ); - } - #[cfg(not(feature = "svg"))] - Primitive::Svg { .. } => { - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {primitive:?}", - ); - } - Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule, - }) => { - let bounds = path.bounds(); - - let physical_bounds = (Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } * transformation) - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - pixels.fill_path( - path, - paint, - *rule, - into_transform(transformation) - .post_scale(scale_factor, scale_factor), - clip_mask, - ); - } - Primitive::Custom(primitive::Custom::Stroke { - path, - paint, - stroke, - }) => { - let bounds = path.bounds(); - - let physical_bounds = (Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width().max(1.0), - height: bounds.height().max(1.0), - } * transformation) - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - pixels.stroke_path( - path, - paint, - stroke, - into_transform(transformation) - .post_scale(scale_factor, scale_factor), - clip_mask, - ); - } - Primitive::Group { primitives } => { - for primitive in primitives { - self.draw_primitive( - primitive, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - self.draw_primitive( - content, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation * *new_transformation, - ); - } - Primitive::Clip { bounds, content } => { - let bounds = (*bounds * transformation) * scale_factor; - - if bounds == clip_bounds { - self.draw_primitive( - content, - pixels, - clip_mask, - bounds, - scale_factor, - transformation, - ); - } else if let Some(bounds) = clip_bounds.intersection(&bounds) { - if bounds.x + bounds.width <= 0.0 - || bounds.y + bounds.height <= 0.0 - || bounds.x as u32 >= pixels.width() - || bounds.y as u32 >= pixels.height() - || bounds.width <= 1.0 - || bounds.height <= 1.0 - { - return; - } - - adjust_clip_mask(clip_mask, bounds); - - self.draw_primitive( - content, - pixels, - clip_mask, - bounds, - scale_factor, - transformation, - ); - - adjust_clip_mask(clip_mask, clip_bounds); - } - } - Primitive::Cache { content } => { - self.draw_primitive( - content, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation, - ); - } - } - } -} - -impl Default for Backend { - fn default() -> Self { - Self::new() - } -} - -fn into_color(color: Color) -> tiny_skia::Color { - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Convert color from iced to tiny_skia") -} - -fn into_transform(transformation: Transformation) -> tiny_skia::Transform { - let translation = transformation.translation(); - - tiny_skia::Transform { - sx: transformation.scale_factor(), - kx: 0.0, - ky: 0.0, - sy: transformation.scale_factor(), - tx: translation.x, - ty: translation.y, - } -} - -fn rounded_rectangle( - bounds: Rectangle, - border_radius: [f32; 4], -) -> tiny_skia::Path { - let [top_left, top_right, bottom_right, bottom_left] = border_radius; - - if top_left == 0.0 - && top_right == 0.0 - && bottom_right == 0.0 - && bottom_left == 0.0 - { - return tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .expect("Build quad rectangle"), - ); - } - - if top_left == top_right - && top_left == bottom_right - && top_left == bottom_left - && top_left == bounds.width / 2.0 - && top_left == bounds.height / 2.0 - { - return tiny_skia::PathBuilder::from_circle( - bounds.x + bounds.width / 2.0, - bounds.y + bounds.height / 2.0, - top_left, - ) - .expect("Build circle path"); - } - - let mut builder = tiny_skia::PathBuilder::new(); - - builder.move_to(bounds.x + top_left, bounds.y); - builder.line_to(bounds.x + bounds.width - top_right, bounds.y); - - if top_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width - top_right, - bounds.y, - bounds.x + bounds.width, - bounds.y + top_right, - top_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - ); - - if bottom_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - bounds.x + bounds.width - bottom_right, - bounds.y + bounds.height, - bottom_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - ); - - if bottom_left > 0.0 { - arc_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - bounds.x, - bounds.y + bounds.height - bottom_left, - bottom_left, - ); - } - - maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); - - if top_left > 0.0 { - arc_to( - &mut builder, - bounds.x, - bounds.y + top_left, - bounds.x + top_left, - bounds.y, - top_left, - ); - } - - builder.finish().expect("Build rounded rectangle path") -} - -fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { - if path.last_point() != Some(tiny_skia::Point { x, y }) { - path.line_to(x, y); - } -} - -fn arc_to( - path: &mut tiny_skia::PathBuilder, - x_from: f32, - y_from: f32, - x_to: f32, - y_to: f32, - radius: f32, -) { - let svg_arc = kurbo::SvgArc { - from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), - to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), - radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), - x_rotation: 0.0, - large_arc: false, - sweep: true, - }; - - match kurbo::Arc::from_svg_arc(&svg_arc) { - Some(arc) => { - arc.to_cubic_beziers(0.1, |p1, p2, p| { - path.cubic_to( - p1.x as f32, - p1.y as f32, - p2.x as f32, - p2.y as f32, - p.x as f32, - p.y as f32, - ); - }); - } - None => { - path.line_to(x_to, y_to); - } - } -} - -fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { - clip_mask.clear(); - - let path = { - let mut builder = tiny_skia::PathBuilder::new(); - builder.push_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .unwrap(), - ); - - builder.finish().unwrap() - }; - - clip_mask.fill_path( - &path, - tiny_skia::FillRule::EvenOdd, - false, - tiny_skia::Transform::default(), - ); -} - -fn smoothstep(a: f32, b: f32, x: f32) -> f32 { - let x = ((x - a) / (b - a)).clamp(0.0, 1.0); - - x * x * (3.0 - 2.0 * x) -} - -fn rounded_box_sdf( - to_center: Vector, - size: tiny_skia::Size, - radii: &[f32], -) -> f32 { - let radius = match (to_center.x > 0.0, to_center.y > 0.0) { - (true, true) => radii[2], - (true, false) => radii[1], - (false, true) => radii[3], - (false, false) => radii[0], - }; - - let x = (to_center.x.abs() - size.width() + radius).max(0.0); - let y = (to_center.y.abs() - size.height() + radius).max(0.0); - - (x.powf(2.0) + y.powf(2.0)).sqrt() - radius -} - -impl iced_graphics::Backend for Backend { - type Primitive = primitive::Custom; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions( - &self, - handle: &crate::core::image::Handle, - ) -> crate::core::Size<u32> { - self.raster_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> crate::core::Size<u32> { - self.vector_pipeline.viewport_dimensions(handle) - } -} diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs new file mode 100644 index 00000000..028b304f --- /dev/null +++ b/tiny_skia/src/engine.rs @@ -0,0 +1,856 @@ +use crate::core::renderer::Quad; +use crate::core::{ + Background, Color, Gradient, Rectangle, Size, Transformation, Vector, +}; +use crate::graphics::{Image, Text}; +use crate::text; +use crate::Primitive; + +#[derive(Debug)] +pub struct Engine { + text_pipeline: text::Pipeline, + + #[cfg(feature = "image")] + pub(crate) raster_pipeline: crate::raster::Pipeline, + #[cfg(feature = "svg")] + pub(crate) vector_pipeline: crate::vector::Pipeline, +} + +impl Engine { + pub fn new() -> Self { + Self { + text_pipeline: text::Pipeline::new(), + #[cfg(feature = "image")] + raster_pipeline: crate::raster::Pipeline::new(), + #[cfg(feature = "svg")] + vector_pipeline: crate::vector::Pipeline::new(), + } + } + + pub fn draw_quad( + &mut self, + quad: &Quad, + background: &Background, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + debug_assert!( + quad.bounds.width.is_normal(), + "Quad with non-normal width!" + ); + debug_assert!( + quad.bounds.height.is_normal(), + "Quad with non-normal height!" + ); + + let physical_bounds = quad.bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let transform = into_transform(transformation); + + // Make sure the border radius is not larger than the bounds + let border_width = quad + .border + .width + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); + + for radius in &mut fill_border_radius { + *radius = (*radius) + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + } + + let path = rounded_rectangle(quad.bounds, fill_border_radius); + + let shadow = quad.shadow; + + if shadow.color.a > 0.0 { + let shadow_bounds = Rectangle { + x: quad.bounds.x + shadow.offset.x - shadow.blur_radius, + y: quad.bounds.y + shadow.offset.y - shadow.blur_radius, + width: quad.bounds.width + shadow.blur_radius * 2.0, + height: quad.bounds.height + shadow.blur_radius * 2.0, + } * transformation; + + let radii = fill_border_radius + .into_iter() + .map(|radius| radius * transformation.scale_factor()) + .collect::<Vec<_>>(); + let (x, y, width, height) = ( + shadow_bounds.x as u32, + shadow_bounds.y as u32, + shadow_bounds.width as u32, + shadow_bounds.height as u32, + ); + let half_width = physical_bounds.width / 2.0; + let half_height = physical_bounds.height / 2.0; + + let colors = (y..y + height) + .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32))) + .filter_map(|(x, y)| { + tiny_skia::Size::from_wh(half_width, half_height).map( + |size| { + let shadow_distance = rounded_box_sdf( + Vector::new( + x - physical_bounds.position().x + - (shadow.offset.x + * transformation.scale_factor()) + - half_width, + y - physical_bounds.position().y + - (shadow.offset.y + * transformation.scale_factor()) + - half_height, + ), + size, + &radii, + ) + .max(0.0); + let shadow_alpha = 1.0 + - smoothstep( + -shadow.blur_radius + * transformation.scale_factor(), + shadow.blur_radius + * transformation.scale_factor(), + shadow_distance, + ); + + let mut color = into_color(shadow.color); + color.apply_opacity(shadow_alpha); + + color.to_color_u8().premultiply() + }, + ) + }) + .collect(); + + if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height) + .and_then(|size| { + tiny_skia::Pixmap::from_vec( + bytemuck::cast_vec(colors), + size, + ) + }) + { + pixels.draw_pixmap( + x as i32, + y as i32, + pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::default(), + None, + ); + } + } + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color(*color)) + } + Background::Gradient(Gradient::Linear(linear)) => { + let (start, end) = + linear.angle.to_distance(&quad.bounds); + + let stops: Vec<tiny_skia::GradientStop> = linear + .stops + .into_iter() + .flatten() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(); + + tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: start.x, + y: start.y, + }, + tiny_skia::Point { x: end.x, y: end.y }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } + }, + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + if border_width > 0.0 { + // Border path is offset by half the border width + let border_bounds = Rectangle { + x: quad.bounds.x + border_width / 2.0, + y: quad.bounds.y + border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + // Make sure the border radius is correct + let mut border_radius = <[f32; 4]>::from(quad.border.radius); + let mut is_simple_border = true; + + for radius in &mut border_radius { + *radius = if *radius == 0.0 { + // Path should handle this fine + 0.0 + } else if *radius > border_width / 2.0 { + *radius - border_width / 2.0 + } else { + is_simple_border = false; + 0.0 + } + .min(border_bounds.width / 2.0) + .min(border_bounds.height / 2.0); + } + + // Stroking a path works well in this case + if is_simple_border { + let border_path = + rounded_rectangle(border_bounds, border_radius); + + pixels.stroke_path( + &border_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } else { + // Draw corners that have too small border radii as having no border radius, + // but mask them with the rounded rectangle with the correct border radius. + let mut temp_pixmap = tiny_skia::Pixmap::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let mut quad_mask = tiny_skia::Mask::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let zero_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: quad.bounds.width, + height: quad.bounds.height, + }; + let path = rounded_rectangle(zero_bounds, fill_border_radius); + + quad_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + true, + transform, + ); + let path_bounds = Rectangle { + x: border_width / 2.0, + y: border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + let border_radius_path = + rounded_rectangle(path_bounds, border_radius); + + temp_pixmap.stroke_path( + &border_radius_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + Some(&quad_mask), + ); + + pixels.draw_pixmap( + quad.bounds.x as i32, + quad.bounds.y as i32, + temp_pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + transform, + clip_mask, + ); + } + } + } + + pub fn draw_text( + &mut self, + text: &Text, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + match text { + Text::Paragraph { + paragraph, + position, + color, + clip_bounds: _, // TODO + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + + let physical_bounds = + Rectangle::new(*position, paragraph.min_bounds) + * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_paragraph( + paragraph, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Editor { + editor, + position, + color, + clip_bounds: _, // TODO + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + + let physical_bounds = + Rectangle::new(*position, editor.bounds) * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_editor( + editor, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Cached { + content, + bounds, + color, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping, + clip_bounds: text_bounds, // TODO + } => { + let physical_bounds = *text_bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_cached( + content, + *bounds, + *color, + *size, + *line_height, + *font, + *horizontal_alignment, + *vertical_alignment, + *shaping, + pixels, + clip_mask, + transformation, + ); + } + Text::Raw { + raw, + transformation: local_transformation, + } => { + let Some(buffer) = raw.buffer.upgrade() else { + return; + }; + + let transformation = transformation * *local_transformation; + let (width, height) = buffer.size(); + + let physical_bounds = + Rectangle::new(raw.position, Size::new(width, height)) + * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + raw.position, + raw.color, + pixels, + clip_mask, + transformation, + ); + } + } + } + + pub fn draw_primitive( + &mut self, + primitive: &Primitive, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + layer_bounds: Rectangle, + ) { + match primitive { + Primitive::Fill { path, paint, rule } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } * transformation + }; + + let Some(clip_bounds) = + layer_bounds.intersection(&physical_bounds) + else { + return; + }; + + let clip_mask = + (physical_bounds != clip_bounds).then_some(clip_mask as &_); + + pixels.fill_path( + path, + paint, + *rule, + into_transform(transformation), + clip_mask, + ); + } + Primitive::Stroke { + path, + paint, + stroke, + } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } * transformation + }; + + let Some(clip_bounds) = + layer_bounds.intersection(&physical_bounds) + else { + return; + }; + + let clip_mask = + (physical_bounds != clip_bounds).then_some(clip_mask as &_); + + pixels.stroke_path( + path, + paint, + stroke, + into_transform(transformation), + clip_mask, + ); + } + } + } + + pub fn draw_image( + &mut self, + image: &Image, + _transformation: Transformation, + _pixels: &mut tiny_skia::PixmapMut<'_>, + _clip_mask: &mut tiny_skia::Mask, + _clip_bounds: Rectangle, + ) { + match image { + #[cfg(feature = "image")] + Image::Raster { + handle, + filter_method, + bounds, + rotation, + opacity, + } => { + let physical_bounds = *bounds * _transformation; + + if !_clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(*rotation); + + let transform = into_transform(_transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); + + self.raster_pipeline.draw( + handle, + *filter_method, + *bounds, + *opacity, + _pixels, + transform, + clip_mask, + ); + } + #[cfg(feature = "svg")] + Image::Vector { + handle, + color, + bounds, + rotation, + opacity, + } => { + let physical_bounds = *bounds * _transformation; + + if !_clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(*rotation); + + let transform = into_transform(_transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); + + self.vector_pipeline.draw( + handle, + *color, + physical_bounds, + *opacity, + _pixels, + transform, + clip_mask, + ); + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + } + } + + pub fn trim(&mut self) { + self.text_pipeline.trim_cache(); + + #[cfg(feature = "image")] + self.raster_pipeline.trim_cache(); + + #[cfg(feature = "svg")] + self.vector_pipeline.trim_cache(); + } +} + +pub fn into_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Convert color from iced to tiny_skia") +} + +fn into_transform(transformation: Transformation) -> tiny_skia::Transform { + let translation = transformation.translation(); + + tiny_skia::Transform { + sx: transformation.scale_factor(), + kx: 0.0, + ky: 0.0, + sy: transformation.scale_factor(), + tx: translation.x, + ty: translation.y, + } +} + +fn rounded_rectangle( + bounds: Rectangle, + border_radius: [f32; 4], +) -> tiny_skia::Path { + let [top_left, top_right, bottom_right, bottom_left] = border_radius; + + if top_left == 0.0 + && top_right == 0.0 + && bottom_right == 0.0 + && bottom_left == 0.0 + { + return tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Build quad rectangle"), + ); + } + + if top_left == top_right + && top_left == bottom_right + && top_left == bottom_left + && top_left == bounds.width / 2.0 + && top_left == bounds.height / 2.0 + { + return tiny_skia::PathBuilder::from_circle( + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + top_left, + ) + .expect("Build circle path"); + } + + let mut builder = tiny_skia::PathBuilder::new(); + + builder.move_to(bounds.x + top_left, bounds.y); + builder.line_to(bounds.x + bounds.width - top_right, bounds.y); + + if top_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width - top_right, + bounds.y, + bounds.x + bounds.width, + bounds.y + top_right, + top_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + + if bottom_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height, + bottom_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + ); + + if bottom_left > 0.0 { + arc_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bottom_left, + bottom_left, + ); + } + + maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); + + if top_left > 0.0 { + arc_to( + &mut builder, + bounds.x, + bounds.y + top_left, + bounds.x + top_left, + bounds.y, + top_left, + ); + } + + builder.finish().expect("Build rounded rectangle path") +} + +fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { + if path.last_point() != Some(tiny_skia::Point { x, y }) { + path.line_to(x, y); + } +} + +fn arc_to( + path: &mut tiny_skia::PathBuilder, + x_from: f32, + y_from: f32, + x_to: f32, + y_to: f32, + radius: f32, +) { + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), + to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), + radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), + x_rotation: 0.0, + large_arc: false, + sweep: true, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + arc.to_cubic_beziers(0.1, |p1, p2, p| { + path.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }); + } + None => { + path.line_to(x_to, y_to); + } + } +} + +fn smoothstep(a: f32, b: f32, x: f32) -> f32 { + let x = ((x - a) / (b - a)).clamp(0.0, 1.0); + + x * x * (3.0 - 2.0 * x) +} + +fn rounded_box_sdf( + to_center: Vector, + size: tiny_skia::Size, + radii: &[f32], +) -> f32 { + let radius = match (to_center.x > 0.0, to_center.y > 0.0) { + (true, true) => radii[2], + (true, false) => radii[1], + (false, true) => radii[3], + (false, false) => radii[0], + }; + + let x = (to_center.x.abs() - size.width() + radius).max(0.0); + let y = (to_center.y.abs() - size.height() + radius).max(0.0); + + (x.powf(2.0) + y.powf(2.0)).sqrt() - radius +} + +pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { + clip_mask.clear(); + + let path = { + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .unwrap(), + ); + + builder.finish().unwrap() + }; + + clip_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + false, + tiny_skia::Transform::default(), + ); +} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index f7518731..02b6e1b9 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,45 +1,102 @@ use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Rectangle, Size, Transformation, Vector}; +use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; -use crate::graphics::geometry::{Path, Style, Text}; -use crate::graphics::Gradient; -use crate::primitive::{self, Primitive}; +use crate::graphics::geometry::{self, Path, Style}; +use crate::graphics::{Gradient, Text}; +use crate::Primitive; +use std::rc::Rc; + +#[derive(Debug)] +pub enum Geometry { + Live { + text: Vec<Text>, + primitives: Vec<Primitive>, + clip_bounds: Rectangle, + }, + Cache(Cache), +} + +#[derive(Debug, Clone)] +pub struct Cache { + pub text: Rc<[Text]>, + pub primitives: Rc<[Primitive]>, + pub clip_bounds: Rectangle, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Cache) -> Self { + Self::Cache(cache.clone()) + } + + fn cache(self, _group: cache::Group, _previous: Option<Cache>) -> Cache { + match self { + Self::Live { + primitives, + text, + clip_bounds, + } => Cache { + primitives: Rc::from(primitives), + text: Rc::from(text), + clip_bounds, + }, + Self::Cache(cache) => cache, + } + } +} + +#[derive(Debug)] pub struct Frame { - size: Size, + clip_bounds: Rectangle, transform: tiny_skia::Transform, stack: Vec<tiny_skia::Transform>, primitives: Vec<Primitive>, + text: Vec<Text>, } impl Frame { pub fn new(size: Size) -> Self { + Self::with_clip(Rectangle::with_size(size)) + } + + pub fn with_clip(clip_bounds: Rectangle) -> Self { Self { - size, - transform: tiny_skia::Transform::identity(), + clip_bounds, stack: Vec::new(), primitives: Vec::new(), + text: Vec::new(), + transform: tiny_skia::Transform::from_translate( + clip_bounds.x, + clip_bounds.y, + ), } } +} - pub fn width(&self) -> f32 { - self.size.width +impl geometry::frame::Backend for Frame { + type Geometry = Geometry; + + fn width(&self) -> f32 { + self.clip_bounds.width } - pub fn height(&self) -> f32 { - self.size.height + fn height(&self) -> f32 { + self.clip_bounds.height } - pub fn size(&self) -> Size { - self.size + fn size(&self) -> Size { + self.clip_bounds.size() } - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) + fn center(&self) -> Point { + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) } - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { @@ -51,15 +108,14 @@ impl Frame { let mut paint = into_paint(fill.style); paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - })); + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); } - pub fn fill_rectangle( + fn fill_rectangle( &mut self, top_left: Point, size: Size, @@ -79,15 +135,14 @@ impl Frame { }; paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - })); + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); } - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { let Some(path) = convert_path(path).and_then(|path| path.transform(self.transform)) else { @@ -100,20 +155,19 @@ impl Frame { let mut paint = into_paint(stroke.style); paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Stroke { - path, - paint, - stroke: skia_stroke, - })); + self.primitives.push(Primitive::Stroke { + path, + paint, + stroke: skia_stroke, + }); } - pub fn fill_text(&mut self, text: impl Into<Text>) { + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); let (scale_x, scale_y) = self.transform.get_scale(); - if self.transform.is_scale_translate() + if !self.transform.has_skew() && scale_x == scale_y && scale_x > 0.0 && scale_y > 0.0 @@ -155,12 +209,12 @@ impl Frame { }; // TODO: Honor layering! - self.primitives.push(Primitive::Text { + self.text.push(Text::Cached { content: text.content, bounds, color: text.color, size, - line_height, + line_height: line_height.to_absolute(size), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, @@ -172,50 +226,51 @@ impl Frame { } } - pub fn push_transform(&mut self) { + fn push_transform(&mut self) { self.stack.push(self.transform); } - pub fn pop_transform(&mut self) { + fn pop_transform(&mut self) { self.transform = self.stack.pop().expect("Pop transform"); } - pub fn clip(&mut self, frame: Self, at: Point) { - self.primitives.push(Primitive::Transform { - transformation: Transformation::translate(at.x, at.y), - content: Box::new(frame.into_primitive()), - }); + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self::with_clip(clip_bounds) } - pub fn translate(&mut self, translation: Vector) { + fn paste(&mut self, frame: Self, _at: Point) { + self.primitives.extend(frame.primitives); + self.text.extend(frame.text); + } + + fn translate(&mut self, translation: Vector) { self.transform = 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())); + 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>) { + fn scale(&mut self, scale: impl Into<f32>) { let scale = scale.into(); self.scale_nonuniform(Vector { x: scale, y: scale }); } - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { let scale = scale.into(); self.transform = self.transform.pre_scale(scale.x, scale.y); } - pub fn into_primitive(self) -> Primitive { - Primitive::Clip { - bounds: Rectangle::new(Point::ORIGIN, self.size), - content: Box::new(Primitive::Group { - primitives: self.primitives, - }), + fn into_geometry(self) -> Geometry { + Geometry::Live { + primitives: self.primitives, + text: self.text, + clip_bounds: self.clip_bounds, } } } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs new file mode 100644 index 00000000..48fca1d8 --- /dev/null +++ b/tiny_skia/src/layer.rs @@ -0,0 +1,341 @@ +use crate::core::{ + image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, + Transformation, +}; +use crate::graphics::damage; +use crate::graphics::layer; +use crate::graphics::text::{Editor, Paragraph, Text}; +use crate::graphics::{self, Image}; +use crate::Primitive; + +use std::rc::Rc; + +pub type Stack = layer::Stack<Layer>; + +#[derive(Debug, Clone)] +pub struct Layer { + pub bounds: Rectangle, + pub quads: Vec<(Quad, Background)>, + pub primitives: Vec<Item<Primitive>>, + pub text: Vec<Item<Text>>, + pub images: Vec<Image>, +} + +impl Layer { + pub fn draw_quad( + &mut self, + mut quad: Quad, + background: Background, + transformation: Transformation, + ) { + quad.bounds = quad.bounds * transformation; + self.quads.push((quad, background)); + } + + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(paragraph)); + } + + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(editor)); + } + + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.text.push(Item::Live(text)); + } + + pub fn draw_text_group( + &mut self, + text: Vec<Text>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Group(text, clip_bounds, transformation)); + } + + pub fn draw_text_cache( + &mut self, + text: Rc<[Text]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Cached(text, clip_bounds, transformation)); + } + + pub fn draw_image( + &mut self, + handle: image::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: svg::Handle, + color: Option<Color>, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(svg); + } + + pub fn draw_primitive_group( + &mut self, + primitives: Vec<Primitive>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Group( + primitives, + clip_bounds, + transformation, + )); + } + + pub fn draw_primitive_cache( + &mut self, + primitives: Rc<[Primitive]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Cached( + primitives, + clip_bounds, + transformation, + )); + } + + pub fn damage(previous: &Self, current: &Self) -> Vec<Rectangle> { + if previous.bounds != current.bounds { + return vec![previous.bounds, current.bounds]; + } + + let mut damage = damage::list( + &previous.quads, + ¤t.quads, + |(quad, _)| { + quad.bounds + .expand(1.0) + .intersection(¤t.bounds) + .into_iter() + .collect() + }, + |(quad_a, background_a), (quad_b, background_b)| { + quad_a == quad_b && background_a == background_b + }, + ); + + let text = damage::diff( + &previous.text, + ¤t.text, + |item| { + item.as_slice() + .iter() + .filter_map(Text::visible_bounds) + .map(|bounds| bounds * item.transformation()) + .collect() + }, + |text_a, text_b| { + damage::list( + text_a.as_slice(), + text_b.as_slice(), + |text| { + text.visible_bounds() + .into_iter() + .map(|bounds| bounds * text_a.transformation()) + .collect() + }, + |text_a, text_b| text_a == text_b, + ) + }, + ); + + let primitives = damage::list( + &previous.primitives, + ¤t.primitives, + |item| match item { + Item::Live(primitive) => vec![primitive.visible_bounds()], + Item::Group(primitives, group_bounds, transformation) => { + primitives + .as_slice() + .iter() + .map(Primitive::visible_bounds) + .map(|bounds| bounds * *transformation) + .filter_map(|bounds| bounds.intersection(group_bounds)) + .collect() + } + Item::Cached(_, bounds, _) => { + vec![*bounds] + } + }, + |primitive_a, primitive_b| match (primitive_a, primitive_b) { + ( + Item::Cached(cache_a, bounds_a, transformation_a), + Item::Cached(cache_b, bounds_b, transformation_b), + ) => { + Rc::ptr_eq(cache_a, cache_b) + && bounds_a == bounds_b + && transformation_a == transformation_b + } + _ => false, + }, + ); + + let images = damage::list( + &previous.images, + ¤t.images, + |image| vec![image.bounds().expand(1.0)], + Image::eq, + ); + + damage.extend(text); + damage.extend(primitives); + damage.extend(images); + damage + } +} + +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, + quads: Vec::new(), + primitives: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } +} + +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { + Self { + bounds, + ..Self::default() + } + } + + fn flush(&mut self) {} + + fn resize(&mut self, bounds: graphics::core::Rectangle) { + self.bounds = bounds; + } + + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; + + self.quads.clear(); + self.primitives.clear(); + self.text.clear(); + self.images.clear(); + } +} + +#[derive(Debug, Clone)] +pub enum Item<T> { + Live(T), + Group(Vec<T>, Rectangle, Transformation), + Cached(Rc<[T]>, Rectangle, Transformation), +} + +impl<T> Item<T> { + pub fn transformation(&self) -> Transformation { + match self { + Item::Live(_) => Transformation::IDENTITY, + Item::Group(_, _, transformation) + | Item::Cached(_, _, transformation) => *transformation, + } + } + + pub fn clip_bounds(&self) -> Rectangle { + match self { + Item::Live(_) => Rectangle::INFINITE, + Item::Group(_, clip_bounds, _) + | Item::Cached(_, clip_bounds, _) => *clip_bounds, + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Item::Live(item) => std::slice::from_ref(item), + Item::Group(group, _, _) => group.as_slice(), + Item::Cached(cache, _, _) => cache, + } + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index e7294f9b..a7fc2d7c 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -1,9 +1,9 @@ -#![forbid(rust_2018_idioms)] -#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)] +#![allow(missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod window; -mod backend; +mod engine; +mod layer; mod primitive; mod settings; mod text; @@ -20,12 +20,357 @@ pub mod geometry; pub use iced_graphics as graphics; pub use iced_graphics::core; -pub use backend::Backend; +pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::renderer; +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Transformation, +}; +use crate::engine::Engine; +use crate::graphics::compositor; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; + /// A [`tiny-skia`] graphics renderer for [`iced`]. /// /// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +#[derive(Debug)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + engine: Engine, // TODO: Shared engine +} + +impl Renderer { + pub fn new(default_font: Font, default_text_size: Pixels) -> Self { + Self { + default_font, + default_text_size, + layers: layer::Stack::new(), + engine: Engine::new(), + } + } + + pub fn layers(&mut self) -> &[Layer] { + self.layers.flush(); + self.layers.as_slice() + } + + pub fn draw( + &mut self, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + viewport: &Viewport, + damage: &[Rectangle], + background_color: Color, + ) { + let scale_factor = viewport.scale_factor() as f32; + + self.layers.flush(); + + for ®ion in damage { + let region = region * scale_factor; + + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + region.x, + region.y, + region.width, + region.height, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(engine::into_color( + background_color, + )), + anti_alias: false, + blend_mode: tiny_skia::BlendMode::Source, + ..Default::default() + }, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), + None, + ); + + for layer in self.layers.iter() { + let Some(clip_bounds) = + region.intersection(&(layer.bounds * scale_factor)) + else { + continue; + }; + + engine::adjust_clip_mask(clip_mask, clip_bounds); + + for (quad, background) in &layer.quads { + self.engine.draw_quad( + quad, + background, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + + for group in &layer.primitives { + let Some(new_clip_bounds) = (group.clip_bounds() + * scale_factor) + .intersection(&clip_bounds) + else { + continue; + }; + + engine::adjust_clip_mask(clip_mask, new_clip_bounds); + + for primitive in group.as_slice() { + self.engine.draw_primitive( + primitive, + group.transformation() + * Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + + engine::adjust_clip_mask(clip_mask, clip_bounds); + } + + for group in &layer.text { + for text in group.as_slice() { + self.engine.draw_text( + text, + group.transformation() + * Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + } + + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + } + } + + self.engine.trim(); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into<Background>, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: core::Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { + primitives, + text, + clip_bounds, + } => { + layer.draw_primitive_group( + primitives, + clip_bounds, + transformation, + ); + + layer.draw_text_group(text, clip_bounds, transformation); + } + Geometry::Cache(cache) => { + layer.draw_primitive_cache( + cache.primitives, + cache.clip_bounds, + transformation, + ); + + layer.draw_text_cache( + cache.text, + cache.clip_bounds, + transformation, + ); + } + } + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, _mesh: graphics::Mesh) { + log::warn!("iced_tiny_skia does not support drawing meshes"); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> crate::core::Size<u32> { + self.engine.raster_pipeline.dimensions(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg( + &self, + handle: &core::svg::Handle, + ) -> crate::core::Size<u32> { + self.engine.vector_pipeline.viewport_dimensions(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color: Option<Color>, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg( + handle, + color, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +impl compositor::Default for Renderer { + type Compositor = window::Compositor; +} diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 7718d542..5de51047 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -1,10 +1,7 @@ use crate::core::Rectangle; -use crate::graphics::Damage; - -pub type Primitive = crate::graphics::Primitive<Custom>; #[derive(Debug, Clone, PartialEq)] -pub enum Custom { +pub enum Primitive { /// A path filled with some paint. Fill { /// The path to fill. @@ -25,20 +22,19 @@ pub enum Custom { }, } -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Fill { path, .. } | Self::Stroke { path, .. } => { - let bounds = path.bounds(); +impl Primitive { + /// Returns the visible bounds of the [`Primitive`]. + pub fn visible_bounds(&self) -> Rectangle { + let bounds = match self { + Primitive::Fill { path, .. } => path.bounds(), + Primitive::Stroke { path, .. } => path.bounds(), + }; - Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } - .expand(1.0) - } + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), } } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 5f17ae60..c40f55b2 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -6,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; use std::collections::hash_map; +#[derive(Debug)] pub struct Pipeline { cache: RefCell<Cache>, } @@ -30,6 +31,7 @@ impl Pipeline { handle: &raster::Handle, filter_method: raster::FilterMethod, bounds: Rectangle, + opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, @@ -55,6 +57,7 @@ impl Pipeline { image, &tiny_skia::PixmapPaint { quality, + opacity, ..Default::default() }, transform, @@ -68,10 +71,10 @@ impl Pipeline { } } -#[derive(Default)] +#[derive(Debug, Default)] struct Cache { - entries: FxHashMap<u64, Option<Entry>>, - hits: FxHashSet<u64>, + entries: FxHashMap<raster::Id, Option<Entry>>, + hits: FxHashSet<raster::Id>, } impl Cache { @@ -82,7 +85,7 @@ impl Cache { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = graphics::image::load(handle).ok()?.into_rgba8(); + let image = graphics::image::load(handle).ok()?; let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; @@ -119,6 +122,7 @@ impl Cache { } } +#[derive(Debug)] struct Entry { width: u32, height: u32, diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index ec27b218..672c49f3 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -1,8 +1,9 @@ use crate::core::{Font, Pixels}; +use crate::graphics; -/// The settings of a [`Backend`]. +/// The settings of a [`Compositor`]. /// -/// [`Backend`]: crate::Backend +/// [`Compositor`]: crate::window::Compositor #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { /// The default [`Font`] to use. @@ -22,3 +23,12 @@ impl Default for Settings { } } } + +impl From<graphics::Settings> for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + } + } +} diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d28cc483..c71deb10 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,5 +1,5 @@ use crate::core::alignment; -use crate::core::text::{LineHeight, Shaping}; +use crate::core::text::Shaping; use crate::core::{ Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; @@ -13,7 +13,7 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map; -#[allow(missing_debug_implementations)] +#[derive(Debug)] pub struct Pipeline { glyph_cache: GlyphCache, cache: RefCell<Cache>, @@ -27,6 +27,8 @@ impl Pipeline { } } + // TODO: Shared engine + #[allow(dead_code)] pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { font_system() .write() @@ -41,7 +43,6 @@ impl Pipeline { paragraph: ¶graph::Weak, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -62,7 +63,6 @@ impl Pipeline { color, paragraph.horizontal_alignment(), paragraph.vertical_alignment(), - scale_factor, pixels, clip_mask, transformation, @@ -74,7 +74,6 @@ impl Pipeline { editor: &editor::Weak, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -95,7 +94,6 @@ impl Pipeline { color, alignment::Horizontal::Left, alignment::Vertical::Top, - scale_factor, pixels, clip_mask, transformation, @@ -108,17 +106,16 @@ impl Pipeline { bounds: Rectangle, color: Color, size: Pixels, - line_height: LineHeight, + line_height: Pixels, font: Font, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { - let line_height = f32::from(line_height.to_absolute(size)); + let line_height = f32::from(line_height); let mut font_system = font_system().write().expect("Write font system"); let font_system = font_system.raw(); @@ -149,7 +146,6 @@ impl Pipeline { color, horizontal_alignment, vertical_alignment, - scale_factor, pixels, clip_mask, transformation, @@ -161,7 +157,6 @@ impl Pipeline { buffer: &cosmic_text::Buffer, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -178,7 +173,6 @@ impl Pipeline { color, alignment::Horizontal::Left, alignment::Vertical::Top, - scale_factor, pixels, clip_mask, transformation, @@ -199,12 +193,11 @@ fn draw( color: Color, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { - let bounds = bounds * transformation * scale_factor; + let bounds = bounds * transformation; let x = match horizontal_alignment { alignment::Horizontal::Left => bounds.x, @@ -222,8 +215,8 @@ fn draw( for run in buffer.layout_runs() { for glyph in run.glyphs { - let physical_glyph = glyph - .physical((x, y), scale_factor * transformation.scale_factor()); + let physical_glyph = + glyph.physical((x, y), transformation.scale_factor()); if let Some((buffer, placement)) = glyph_cache.allocate( physical_glyph.cache_key, @@ -247,10 +240,8 @@ fn draw( pixels.draw_pixmap( physical_glyph.x + placement.left, physical_glyph.y - placement.top - + (run.line_y - * scale_factor - * transformation.scale_factor()) - .round() as i32, + + (run.line_y * transformation.scale_factor()).round() + as i32, pixmap, &tiny_skia::PixmapPaint { opacity, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index fd1ab3de..bbe08cb8 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -4,11 +4,13 @@ use crate::graphics::text; use resvg::usvg::{self, TreeTextToPath}; use rustc_hash::{FxHashMap, FxHashSet}; +use tiny_skia::Transform; use std::cell::RefCell; use std::collections::hash_map; use std::fs; +#[derive(Debug)] pub struct Pipeline { cache: RefCell<Cache>, } @@ -32,7 +34,9 @@ impl Pipeline { handle: &Handle, color: Option<Color>, bounds: Rectangle, + opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, + transform: Transform, clip_mask: Option<&tiny_skia::Mask>, ) { if let Some(image) = self.cache.borrow_mut().draw( @@ -44,8 +48,11 @@ impl Pipeline { bounds.x as i32, bounds.y as i32, image, - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, + transform, clip_mask, ); } @@ -203,3 +210,13 @@ impl Cache { self.raster_hits.clear(); } } + +impl std::fmt::Debug for Cache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cache") + .field("tree_hits", &self.tree_hits) + .field("rasters", &self.rasters) + .field("raster_hits", &self.raster_hits) + .finish_non_exhaustive() + } +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 09531a5d..2ad90a73 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,43 +1,55 @@ use crate::core::{Color, Rectangle, Size}; use crate::graphics::compositor::{self, Information}; use crate::graphics::damage; -use crate::graphics::{Error, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; +use crate::graphics::error::{self, Error}; +use crate::graphics::{self, Viewport}; +use crate::{Layer, Renderer, Settings}; use std::collections::VecDeque; use std::num::NonZeroU32; +#[allow(missing_debug_implementations)] pub struct Compositor { context: softbuffer::Context<Box<dyn compositor::Window>>, settings: Settings, } +#[allow(missing_debug_implementations)] pub struct Surface { window: softbuffer::Surface< Box<dyn compositor::Window>, Box<dyn compositor::Window>, >, clip_mask: tiny_skia::Mask, - primitive_stack: VecDeque<Vec<Primitive>>, + layer_stack: VecDeque<Vec<Layer>>, background_color: Color, max_age: u8, } impl crate::graphics::Compositor for Compositor { - type Settings = Settings; type Renderer = Renderer; type Surface = Surface; - fn new<W: compositor::Window>( - settings: Self::Settings, + async fn with_backend<W: compositor::Window>( + settings: graphics::Settings, compatible_window: W, + backend: Option<&str>, ) -> Result<Self, Error> { - Ok(new(settings, compatible_window)) + match backend { + None | Some("tiny-skia") | Some("tiny_skia") => { + Ok(new(settings.into(), compatible_window)) + } + Some(backend) => Err(Error::GraphicsAdapterNotFound { + backend: "tiny-skia", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } } fn create_renderer(&self) -> Self::Renderer { Renderer::new( - Backend::new(), self.settings.default_font, self.settings.default_text_size, ) @@ -59,7 +71,7 @@ impl crate::graphics::Compositor for Compositor { window, clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), - primitive_stack: VecDeque::new(), + layer_stack: VecDeque::new(), background_color: Color::BLACK, max_age: 0, }; @@ -85,7 +97,7 @@ impl crate::graphics::Compositor for Compositor { surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); - surface.primitive_stack.clear(); + surface.layer_stack.clear(); } fn fetch_information(&self) -> Information { @@ -102,9 +114,7 @@ impl crate::graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present(backend, surface, primitives, viewport, background_color) - }) + present(renderer, surface, viewport, background_color) } fn screenshot( @@ -114,9 +124,7 @@ impl crate::graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, ) -> Vec<u8> { - renderer.with_primitives(|backend, primitives| { - screenshot(surface, backend, primitives, viewport, background_color) - }) + screenshot(renderer, surface, viewport, background_color) } } @@ -132,37 +140,41 @@ pub fn new<W: compositor::Window>( } pub fn present( - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut Surface, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; let mut buffer = surface .window .buffer_mut() .map_err(|_| compositor::SurfaceError::Lost)?; - let last_primitives = { + let last_layers = { let age = buffer.age(); surface.max_age = surface.max_age.max(age); - surface.primitive_stack.truncate(surface.max_age as usize); + surface.layer_stack.truncate(surface.max_age as usize); if age > 0 { - surface.primitive_stack.get(age as usize - 1) + surface.layer_stack.get(age as usize - 1) } else { None } }; - let damage = last_primitives - .and_then(|last_primitives| { - (surface.background_color == background_color) - .then(|| damage::list(last_primitives, primitives)) + let damage = last_layers + .and_then(|last_layers| { + (surface.background_color == background_color).then(|| { + damage::diff( + last_layers, + renderer.layers(), + |layer| vec![layer.bounds], + Layer::damage, + ) + }) }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); @@ -170,10 +182,11 @@ pub fn present( return Ok(()); } - surface.primitive_stack.push_front(primitives.to_vec()); + surface.layer_stack.push_front(renderer.layers().to_vec()); surface.background_color = background_color; - let damage = damage::group(damage, scale_factor, physical_size); + let damage = + damage::group(damage, Rectangle::with_size(viewport.logical_size())); let mut pixels = tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut buffer), @@ -182,10 +195,9 @@ pub fn present( ) .expect("Create pixel map"); - backend.draw( + renderer.draw( &mut pixels, &mut surface.clip_mask, - primitives, viewport, &damage, background_color, @@ -195,9 +207,8 @@ pub fn present( } pub fn screenshot( + renderer: &mut Renderer, surface: &mut Surface, - backend: &mut Backend, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, ) -> Vec<u8> { @@ -206,7 +217,7 @@ pub fn screenshot( let mut offscreen_buffer: Vec<u32> = vec![0; size.width as usize * size.height as usize]; - backend.draw( + renderer.draw( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), size.width, @@ -214,7 +225,6 @@ pub fn screenshot( ) .expect("Create offscreen pixel map"), &mut surface.clip_mask, - primitives, viewport, &[Rectangle::with_size(Size::new( size.width as f32, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 4a0d89f0..30545fa2 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -32,6 +35,8 @@ glyphon.workspace = true guillotiere.workspace = true log.workspace = true once_cell.workspace = true +rustc-hash.workspace = true +thiserror.workspace = true wgpu.workspace = true lyon.workspace = true @@ -39,6 +44,3 @@ lyon.optional = true resvg.workspace = true resvg.optional = true - -tracing.workspace = true -tracing.optional = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs deleted file mode 100644 index 8ce0b26d..00000000 --- a/wgpu/src/backend.rs +++ /dev/null @@ -1,394 +0,0 @@ -use crate::core::{Color, Size, Transformation}; -use crate::graphics::backend; -use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::pipeline; -use crate::primitive::{self, Primitive}; -use crate::quad; -use crate::text; -use crate::triangle; -use crate::{Layer, Settings}; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image; - -use std::borrow::Cow; - -/// A [`wgpu`] graphics backend for [`iced`]. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -/// [`iced`]: https://github.com/iced-rs/iced -#[allow(missing_debug_implementations)] -pub struct Backend { - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - pipeline_storage: pipeline::Storage, - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, -} - -impl Backend { - /// Creates a new [`Backend`]. - pub fn new( - _adapter: &wgpu::Adapter, - device: &wgpu::Device, - queue: &wgpu::Queue, - settings: Settings, - format: wgpu::TextureFormat, - ) -> Self { - let text_pipeline = text::Pipeline::new(device, queue, format); - let quad_pipeline = quad::Pipeline::new(device, format); - let triangle_pipeline = - triangle::Pipeline::new(device, format, settings.antialiasing); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = { - let backend = _adapter.get_info().backend; - - image::Pipeline::new(device, format, backend) - }; - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - pipeline_storage: pipeline::Storage::default(), - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - } - } - - /// Draws the provided primitives in the given `TextureView`. - /// - /// The text provided as overlay will be rendered on top of the primitives. - /// This is useful for rendering debug information. - pub fn present( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - clear_color: Option<Color>, - format: wgpu::TextureFormat, - frame: &wgpu::TextureView, - primitives: &[Primitive], - viewport: &Viewport, - ) { - log::debug!("Drawing"); - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Backend", "PRESENT").entered(); - - let target_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let transformation = viewport.projection(); - - let layers = Layer::generate(primitives, viewport); - - self.prepare( - device, - queue, - format, - encoder, - scale_factor, - target_size, - transformation, - &layers, - ); - - self.render( - device, - encoder, - frame, - clear_color, - scale_factor, - target_size, - &layers, - ); - - self.quad_pipeline.end_frame(); - self.text_pipeline.end_frame(); - self.triangle_pipeline.end_frame(); - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.end_frame(); - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - _encoder: &mut wgpu::CommandEncoder, - scale_factor: f32, - target_size: Size<u32>, - transformation: Transformation, - layers: &[Layer<'_>], - ) { - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.prepare( - device, - queue, - &layer.quads, - transformation, - scale_factor, - ); - } - - if !layer.meshes.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.triangle_pipeline.prepare( - device, - queue, - &layer.meshes, - scaled, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.image_pipeline.prepare( - device, - queue, - _encoder, - &layer.images, - scaled, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - self.text_pipeline.prepare( - device, - queue, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - } - - if !layer.pipelines.is_empty() { - for pipeline in &layer.pipelines { - pipeline.primitive.prepare( - format, - device, - queue, - pipeline.bounds, - target_size, - scale_factor, - &mut self.pipeline_storage, - ); - } - } - } - } - - fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - clear_color: Option<Color>, - scale_factor: f32, - target_size: Size<u32>, - layers: &[Layer<'_>], - ) { - use std::mem::ManuallyDrop; - - let mut quad_layer = 0; - let mut triangle_layer = 0; - #[cfg(any(feature = "image", feature = "svg"))] - let mut image_layer = 0; - let mut text_layer = 0; - - let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: match clear_color { - Some(background_color) => wgpu::LoadOp::Clear({ - let [r, g, b, a] = - color::pack(background_color).components(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - None => wgpu::LoadOp::Load, - }, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.render( - quad_layer, - bounds, - &layer.quads, - &mut render_pass, - ); - - quad_layer += 1; - } - - if !layer.meshes.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - self.triangle_pipeline.render( - device, - encoder, - target, - triangle_layer, - target_size, - &layer.meshes, - scale_factor, - ); - - triangle_layer += 1; - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - self.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } - } - - if !layer.text.is_empty() { - self.text_pipeline - .render(text_layer, bounds, &mut render_pass); - - text_layer += 1; - } - - if !layer.pipelines.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - for pipeline in &layer.pipelines { - let viewport = (pipeline.viewport * scale_factor).snap(); - - if viewport.width < 1 || viewport.height < 1 { - continue; - } - - pipeline.primitive.render( - &self.pipeline_storage, - target, - target_size, - viewport, - encoder, - ); - } - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - } - - let _ = ManuallyDrop::into_inner(render_pass); - } -} - -impl crate::graphics::Backend for Backend { - type Primitive = primitive::Custom; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { - self.image_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size<u32> { - self.image_pipeline.viewport_dimensions(handle) - } -} diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index ef00c58f..463ea24a 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -1,6 +1,13 @@ use std::marker::PhantomData; +use std::num::NonZeroU64; use std::ops::RangeBounds; +pub const MAX_WRITE_SIZE: usize = 100 * 1024; + +#[allow(unsafe_code)] +const MAX_WRITE_SIZE_U64: NonZeroU64 = + unsafe { NonZeroU64::new_unchecked(MAX_WRITE_SIZE as u64) }; + #[derive(Debug)] pub struct Buffer<T> { label: &'static str, @@ -61,12 +68,46 @@ impl<T: bytemuck::Pod> Buffer<T> { /// Returns the size of the written bytes. pub fn write( &mut self, - queue: &wgpu::Queue, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, offset: usize, contents: &[T], ) -> usize { let bytes: &[u8] = bytemuck::cast_slice(contents); - queue.write_buffer(&self.raw, offset as u64, bytes); + let mut bytes_written = 0; + + // Split write into multiple chunks if necessary + while bytes_written + MAX_WRITE_SIZE < bytes.len() { + belt.write_buffer( + encoder, + &self.raw, + (offset + bytes_written) as u64, + MAX_WRITE_SIZE_U64, + device, + ) + .copy_from_slice( + &bytes[bytes_written..bytes_written + MAX_WRITE_SIZE], + ); + + bytes_written += MAX_WRITE_SIZE; + } + + // There will always be some bytes left, since the previous + // loop guarantees `bytes_written < bytes.len()` + let bytes_left = ((bytes.len() - bytes_written) as u64) + .try_into() + .expect("non-empty write"); + + // Write them + belt.write_buffer( + encoder, + &self.raw, + (offset + bytes_written) as u64, + bytes_left, + device, + ) + .copy_from_slice(&bytes[bytes_written..]); self.offsets.push(offset as u64); diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 4598b0a6..9d593d9c 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; +use wgpu::util::DeviceExt; + pub fn convert( device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -15,28 +17,58 @@ pub fn convert( ..wgpu::SamplerDescriptor::default() }); - //sampler in 0 - let sampler_layout = + #[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] + #[repr(C)] + struct Ratio { + u: f32, + v: f32, + } + + let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + contents: bytemuck::bytes_of(&Ratio { u: 1.0, v: 1.0 }), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }); + + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.sampler_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); - let sampler_bind_group = + let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu.offscreen.sampler.bind_group"), - layout: &sampler_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + layout: &constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], }); let texture_layout = @@ -59,7 +91,7 @@ pub fn convert( let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), - bind_group_layouts: &[&sampler_layout, &texture_layout], + bind_group_layouts: &[&constant_layout, &texture_layout], push_constant_ranges: &[], }); @@ -152,16 +184,9 @@ pub fn convert( }); pass.set_pipeline(&pipeline); - pass.set_bind_group(0, &sampler_bind_group, &[]); + pass.set_bind_group(0, &constant_bind_group, &[]); pass.set_bind_group(1, &texture_bind_group, &[]); pass.draw(0..6, 0..1); texture } - -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -struct Vertex { - ndc: [f32; 2], - uv: [f32; 2], -} diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs new file mode 100644 index 00000000..782fd58c --- /dev/null +++ b/wgpu/src/engine.rs @@ -0,0 +1,87 @@ +use crate::buffer; +use crate::graphics::Antialiasing; +use crate::primitive; +use crate::quad; +use crate::text; +use crate::triangle; + +#[allow(missing_debug_implementations)] +pub struct Engine { + pub(crate) staging_belt: wgpu::util::StagingBelt, + pub(crate) format: wgpu::TextureFormat, + + pub(crate) quad_pipeline: quad::Pipeline, + pub(crate) text_pipeline: text::Pipeline, + pub(crate) triangle_pipeline: triangle::Pipeline, + #[cfg(any(feature = "image", feature = "svg"))] + pub(crate) image_pipeline: crate::image::Pipeline, + pub(crate) primitive_storage: primitive::Storage, +} + +impl Engine { + pub fn new( + _adapter: &wgpu::Adapter, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily + ) -> Self { + let text_pipeline = text::Pipeline::new(device, queue, format); + let quad_pipeline = quad::Pipeline::new(device, format); + let triangle_pipeline = + triangle::Pipeline::new(device, format, antialiasing); + + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = { + let backend = _adapter.get_info().backend; + + crate::image::Pipeline::new(device, format, backend) + }; + + Self { + // TODO: Resize belt smartly (?) + // It would be great if the `StagingBelt` API exposed methods + // for introspection to detect when a resize may be worth it. + staging_belt: wgpu::util::StagingBelt::new( + buffer::MAX_WRITE_SIZE as u64, + ), + format, + + quad_pipeline, + text_pipeline, + triangle_pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + + primitive_storage: primitive::Storage::default(), + } + } + + #[cfg(any(feature = "image", feature = "svg"))] + pub fn create_image_cache( + &self, + device: &wgpu::Device, + ) -> crate::image::Cache { + self.image_pipeline.create_cache(device) + } + + pub fn submit( + &mut self, + queue: &wgpu::Queue, + encoder: wgpu::CommandEncoder, + ) -> wgpu::SubmissionIndex { + self.staging_belt.finish(); + let index = queue.submit(Some(encoder.finish())); + self.staging_belt.recall(); + + self.quad_pipeline.end_frame(); + self.text_pipeline.end_frame(); + self.triangle_pipeline.end_frame(); + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.end_frame(); + + index + } +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 8cfcfff0..f6213e1d 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,30 +1,358 @@ //! 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::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::primitive::{self, Primitive}; +use crate::graphics::{self, Text}; +use crate::text; +use crate::triangle; use lyon::geom::euclid; use lyon::tessellation; + use std::borrow::Cow; +#[derive(Debug)] +pub enum Geometry { + Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Cached(Cache), +} + +#[derive(Debug, Clone)] +pub struct Cache { + pub meshes: Option<triangle::Cache>, + pub text: Option<text::Cache>, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Self::Cache) -> Self { + Geometry::Cached(cache.clone()) + } + + fn cache( + self, + group: cache::Group, + previous: Option<Self::Cache>, + ) -> Self::Cache { + match self { + Self::Live { meshes, text } => { + if let Some(mut previous) = previous { + if let Some(cache) = &mut previous.meshes { + cache.update(meshes); + } else { + previous.meshes = triangle::Cache::new(meshes); + } + + if let Some(cache) = &mut previous.text { + cache.update(text); + } else { + previous.text = text::Cache::new(group, text); + } + + previous + } else { + Cache { + meshes: triangle::Cache::new(meshes), + text: text::Cache::new(group, text), + } + } + } + Self::Cached(cache) => cache, + } + } +} + /// A frame for drawing some geometry. #[allow(missing_debug_implementations)] pub struct Frame { - size: Size, + clip_bounds: Rectangle, buffers: BufferStack, - primitives: Vec<Primitive>, + meshes: Vec<Mesh>, + text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, stroke_tessellator: tessellation::StrokeTessellator, } +impl Frame { + /// Creates a new [`Frame`] with the given [`Size`]. + pub fn new(size: Size) -> Frame { + Self::with_clip(Rectangle::with_size(size)) + } + + /// Creates a new [`Frame`] with the given clip bounds. + pub fn with_clip(bounds: Rectangle) -> Frame { + Frame { + clip_bounds: bounds, + buffers: BufferStack::new(), + meshes: Vec::new(), + text: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform(lyon::math::Transform::translation( + bounds.x, bounds.y, + )), + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } +} + +impl geometry::frame::Backend for Frame { + type Geometry = Geometry; + + #[inline] + fn width(&self) -> f32 { + self.clip_bounds.width + } + + #[inline] + fn height(&self) -> f32 { + self.clip_bounds.height + } + + #[inline] + fn size(&self) -> Size { + self.clip_bounds.size() + } + + #[inline] + fn center(&self) -> Point { + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) + } + + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity() { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.0); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity() { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.0); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + fn fill_text(&mut self, text: impl Into<geometry::Text>) { + let text = text.into(); + + let (scale_x, scale_y) = self.transforms.current.scale(); + + if self.transforms.current.is_scale_translation() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = + if self.transforms.current.is_identity() { + (text.position, text.size, text.line_height) + } else { + let position = + self.transforms.current.transform_point(text.position); + + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + (position, size, line_height) + }; + + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; + + self.text.push(graphics::Text::Cached { + content: text.content, + bounds, + color: text.color, + size, + line_height: line_height.to_absolute(size), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: self.clip_bounds, + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } + } + + #[inline] + fn translate(&mut self, translation: Vector) { + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + } + + #[inline] + fn rotate(&mut self, angle: impl Into<Radians>) { + self.transforms.current.0 = self + .transforms + .current + .0 + .pre_rotate(lyon::math::Angle::radians(angle.into().0)); + } + + #[inline] + fn scale(&mut self, scale: impl Into<f32>) { + let scale = scale.into(); + + self.scale_nonuniform(Vector { x: scale, y: scale }); + } + + #[inline] + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + let scale = scale.into(); + + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); + } + + fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + fn draft(&mut self, clip_bounds: Rectangle) -> Frame { + Frame::with_clip(clip_bounds) + } + + fn paste(&mut self, frame: Frame, _at: Point) { + self.meshes + .extend(frame.buffers.into_meshes(frame.clip_bounds)); + + self.text.extend(frame.text); + } + + fn into_geometry(mut self) -> Self::Geometry { + self.meshes + .extend(self.buffers.into_meshes(self.clip_bounds)); + + Geometry::Live { + meshes: self.meshes, + text: self.text, + } + } +} + enum Buffer { Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), @@ -107,6 +435,34 @@ impl BufferStack { _ => unreachable!(), } } + + fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> { + self.stack + .into_iter() + .filter_map(move |buffer| match buffer { + Buffer::Solid(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + _ => None, + }) + } } #[derive(Debug)] @@ -163,386 +519,6 @@ impl Transform { gradient } } - -impl Frame { - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform(lyon::math::Transform::identity()), - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - if self.transforms.current.is_identity() { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.0); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into<Fill>, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = self - .transforms - .current - .0 - .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); - - let size = - self.transforms.current.0.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = into_line_cap(stroke.line_cap); - options.end_cap = into_line_cap(stroke.line_cap); - options.line_join = into_line_join(stroke.line_join); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity() { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.0); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a `Canvas`. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - pub fn fill_text(&mut self, text: impl Into<Text>) { - let text = text.into(); - - let (scale_x, scale_y) = self.transforms.current.scale(); - - if self.transforms.current.is_scale_translation() - && scale_x == scale_y - && scale_x > 0.0 - && scale_y > 0.0 - { - let (position, size, line_height) = - if self.transforms.current.is_identity() { - (text.position, text.size, text.line_height) - } else { - let position = - self.transforms.current.transform_point(text.position); - - let size = Pixels(text.size.0 * scale_y); - - let line_height = match text.line_height { - LineHeight::Absolute(size) => { - LineHeight::Absolute(Pixels(size.0 * scale_y)) - } - LineHeight::Relative(factor) => { - LineHeight::Relative(factor) - } - }; - - (position, size, line_height) - }; - - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - - // TODO: Honor layering! - self.primitives.push(Primitive::Text { - content: text.content, - bounds, - color: text.color, - size, - line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }); - } else { - text.draw_with(|path, color| self.fill(&path, color)); - } - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R { - self.push_transform(); - - let result = f(self); - - self.pop_transform(); - - result - } - - /// Pushes the current transform in the transform stack. - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - /// Pops a transform from the transform stack and sets it as the current transform. - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip<R>( - &mut self, - region: Rectangle, - f: impl FnOnce(&mut Frame) -> R, - ) -> R { - let mut frame = Frame::new(region.size()); - - let result = f(&mut frame); - - let origin = Point::new(region.x, region.y); - - self.clip(frame, origin); - - result - } - - /// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`]. - pub fn clip(&mut self, frame: Frame, at: Point) { - let size = frame.size(); - let primitives = frame.into_primitives(); - let transformation = Transformation::translate(at.x, at.y); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Transform { - transformation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Transform { - transformation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.0 = - self.transforms - .current - .0 - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.0 = self - .transforms - .current - .0 - .pre_rotate(lyon::math::Angle::radians(angle)); - } - - /// Applies a uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: impl Into<f32>) { - let scale = scale.into(); - - self.scale_nonuniform(Vector { x: scale, y: scale }); - } - - /// Applies a non-uniform scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { - let scale = scale.into(); - - self.transforms.current.0 = - self.transforms.current.0.pre_scale(scale.x, scale.y); - } - - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), - } - } - - fn into_primitives(mut self) -> Vec<Primitive> { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - Buffer::Gradient(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - } - } - - self.primitives - } -} - struct GradientVertex2DBuilder { gradient: gradient::Packed, } @@ -649,7 +625,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { let mut draw_line = false; walk_along_path( - path.raw().iter().flattened(0.01), + path.raw().iter().flattened( + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + ), 0.0, lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, &mut RepeatedPattern { diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index ea36e06d..a1ec0d7b 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -15,15 +15,23 @@ pub const SIZE: u32 = 2048; use crate::core::Size; use crate::graphics::color; +use std::sync::Arc; + #[derive(Debug)] pub struct Atlas { texture: wgpu::Texture, texture_view: wgpu::TextureView, + texture_bind_group: wgpu::BindGroup, + texture_layout: Arc<wgpu::BindGroupLayout>, layers: Vec<Layer>, } impl Atlas { - pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: Arc<wgpu::BindGroupLayout>, + ) -> 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` @@ -60,15 +68,27 @@ impl Atlas { ..Default::default() }); + let texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout: &texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture_view), + }], + }); + Atlas { texture, texture_view, + texture_bind_group, + texture_layout, layers, } } - pub fn view(&self) -> &wgpu::TextureView { - &self.texture_view + pub fn bind_group(&self) -> &wgpu::BindGroup { + &self.texture_bind_group } pub fn layer_count(&self) -> usize { @@ -94,7 +114,7 @@ impl Atlas { entry }; - log::info!("Allocated atlas entry: {entry:?}"); + log::debug!("Allocated atlas entry: {entry:?}"); // It is a webgpu requirement that: // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 @@ -147,13 +167,20 @@ impl Atlas { } } - log::info!("Current atlas: {self:?}"); + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "Atlas layers: {} (busy: {}, allocations: {})", + self.layer_count(), + self.layers.iter().filter(|layer| !layer.is_empty()).count(), + self.layers.iter().map(Layer::allocations).sum::<usize>(), + ); + } Some(entry) } pub fn remove(&mut self, entry: &Entry) { - log::info!("Removing atlas entry: {entry:?}"); + log::debug!("Removing atlas entry: {entry:?}"); match entry { Entry::Contiguous(allocation) => { @@ -266,7 +293,7 @@ impl Atlas { } fn deallocate(&mut self, allocation: &Allocation) { - log::info!("Deallocating atlas: {allocation:?}"); + log::debug!("Deallocating atlas: {allocation:?}"); match allocation { Allocation::Full { layer } => { @@ -414,5 +441,17 @@ impl Atlas { dimension: Some(wgpu::TextureViewDimension::D2Array), ..Default::default() }); + + self.texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout: &self.texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.texture_view, + ), + }], + }); } } diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs index 204a5c26..a51ac1f5 100644 --- a/wgpu/src/image/atlas/allocator.rs +++ b/wgpu/src/image/atlas/allocator.rs @@ -33,6 +33,10 @@ impl Allocator { pub fn is_empty(&self) -> bool { self.allocations == 0 } + + pub fn allocations(&self) -> usize { + self.allocations + } } pub struct Region { diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs index cf089601..fd6788d9 100644 --- a/wgpu/src/image/atlas/layer.rs +++ b/wgpu/src/image/atlas/layer.rs @@ -11,4 +11,12 @@ impl Layer { pub fn is_empty(&self) -> bool { matches!(self, Layer::Empty) } + + pub fn allocations(&self) -> usize { + match self { + Layer::Empty => 0, + Layer::Busy(allocator) => allocator.allocations(), + Layer::Full => 1, + } + } } diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs new file mode 100644 index 00000000..94f7071d --- /dev/null +++ b/wgpu/src/image/cache.rs @@ -0,0 +1,86 @@ +use crate::core::{self, Size}; +use crate::image::atlas::{self, Atlas}; + +use std::sync::Arc; + +#[derive(Debug)] +pub struct Cache { + atlas: Atlas, + #[cfg(feature = "image")] + raster: crate::image::raster::Cache, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache, +} + +impl Cache { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + layout: Arc<wgpu::BindGroupLayout>, + ) -> Self { + Self { + atlas: Atlas::new(device, backend, layout), + #[cfg(feature = "image")] + raster: crate::image::raster::Cache::default(), + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache::default(), + } + } + + pub fn bind_group(&self) -> &wgpu::BindGroup { + self.atlas.bind_group() + } + + pub fn layer_count(&self) -> usize { + self.atlas.layer_count() + } + + #[cfg(feature = "image")] + pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> { + self.raster.load(handle).dimensions() + } + + #[cfg(feature = "svg")] + pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> { + self.vector.load(handle).viewport_dimensions() + } + + #[cfg(feature = "image")] + pub fn upload_raster( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::image::Handle, + ) -> Option<&atlas::Entry> { + self.raster.upload(device, encoder, handle, &mut self.atlas) + } + + #[cfg(feature = "svg")] + pub fn upload_vector( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::svg::Handle, + color: Option<core::Color>, + size: [f32; 2], + scale: f32, + ) -> Option<&atlas::Entry> { + self.vector.upload( + device, + encoder, + handle, + color, + size, + scale, + &mut self.atlas, + ) + } + + pub fn trim(&mut self) { + #[cfg(feature = "image")] + self.raster.trim(&mut self.atlas); + + #[cfg(feature = "svg")] + self.vector.trim(&mut self.atlas); + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image/mod.rs similarity index 75% rename from wgpu/src/image.rs rename to wgpu/src/image/mod.rs index c8e4a4c2..daa2fe16 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod cache; +pub(crate) use cache::Cache; + mod atlas; #[cfg(feature = "image")] @@ -6,175 +9,30 @@ mod raster; #[cfg(feature = "svg")] mod vector; -use atlas::Atlas; - use crate::core::{Rectangle, Size, Transformation}; -use crate::layer; use crate::Buffer; -use std::cell::RefCell; -use std::mem; - use bytemuck::{Pod, Zeroable}; -#[cfg(feature = "image")] -use crate::core::image; +use std::mem; +use std::sync::Arc; -#[cfg(feature = "svg")] -use crate::core::svg; +pub use crate::graphics::Image; -#[cfg(feature = "tracing")] -use tracing::info_span; +pub type Batch = Vec<Image>; #[derive(Debug)] pub struct Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell<raster::Cache>, - #[cfg(feature = "svg")] - vector_cache: RefCell<vector::Cache>, - pipeline: wgpu::RenderPipeline, + backend: wgpu::Backend, nearest_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler, - texture: wgpu::BindGroup, - texture_version: usize, - texture_atlas: Atlas, - texture_layout: wgpu::BindGroupLayout, + texture_layout: Arc<wgpu::BindGroupLayout>, constant_layout: wgpu::BindGroupLayout, - layers: Vec<Layer>, prepare_layer: usize, } -#[derive(Debug)] -struct Layer { - uniforms: wgpu::Buffer, - nearest: Data, - linear: Data, -} - -impl Layer { - fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - nearest_sampler: &wgpu::Sampler, - linear_sampler: &wgpu::Sampler, - ) -> Self { - let uniforms = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image uniforms buffer"), - size: mem::size_of::<Uniforms>() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let nearest = - Data::new(device, constant_layout, nearest_sampler, &uniforms); - - let linear = - Data::new(device, constant_layout, linear_sampler, &uniforms); - - Self { - uniforms, - nearest, - linear, - } - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - nearest_instances: &[Instance], - linear_instances: &[Instance], - transformation: Transformation, - ) { - queue.write_buffer( - &self.uniforms, - 0, - bytemuck::bytes_of(&Uniforms { - transform: transformation.into(), - }), - ); - - self.nearest.upload(device, queue, nearest_instances); - self.linear.upload(device, queue, linear_instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - self.nearest.render(render_pass); - self.linear.render(render_pass); - } -} - -#[derive(Debug)] -struct Data { - constants: wgpu::BindGroup, - instances: Buffer<Instance>, - instance_count: usize, -} - -impl Data { - pub fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, - uniforms: &wgpu::Buffer, - ) -> Self { - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image constants bind group"), - layout: constant_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniforms, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - let instances = Buffer::new( - device, - "iced_wgpu::image instance buffer", - Instance::INITIAL, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - Self { - constants, - instances, - instance_count: 0, - } - } - - fn upload( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - instances: &[Instance], - ) { - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); - - self.instance_count = instances.len(); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(0, self.instances.slice(..)); - - render_pass.draw(0..6, 0..self.instance_count as u32); - } -} - impl Pipeline { pub fn new( device: &wgpu::Device, @@ -257,9 +115,9 @@ impl Pipeline { label: Some("iced_wgpu image shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( concat!( - include_str!("shader/vertex.wgsl"), + include_str!("../shader/vertex.wgsl"), "\n", - include_str!("shader/image.wgsl"), + include_str!("../shader/image.wgsl"), ), )), }); @@ -277,14 +135,20 @@ impl Pipeline { attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Scale + // Center 1 => Float32x2, - // Atlas position + // Scale 2 => Float32x2, + // Rotation + 3 => Float32, + // Opacity + 4 => Float32, + // Atlas position + 5 => Float32x2, // Atlas scale - 3 => Float32x2, + 6 => Float32x2, // Layer - 4 => Sint32, + 7 => Sint32, ), }], }, @@ -322,137 +186,95 @@ impl Pipeline { multiview: None, }); - let texture_atlas = Atlas::new(device, backend); - - let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - texture_atlas.view(), - ), - }], - }); - Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell::new(raster::Cache::default()), - - #[cfg(feature = "svg")] - vector_cache: RefCell::new(vector::Cache::default()), - pipeline, + backend, nearest_sampler, linear_sampler, - texture, - texture_version: texture_atlas.layer_count(), - texture_atlas, - texture_layout, + texture_layout: Arc::new(texture_layout), constant_layout, - layers: Vec::new(), prepare_layer: 0, } } - #[cfg(feature = "image")] - pub fn dimensions(&self, handle: &image::Handle) -> Size<u32> { - let mut cache = self.raster_cache.borrow_mut(); - let memory = cache.load(handle); - - memory.dimensions() - } - - #[cfg(feature = "svg")] - pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32> { - let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(handle); - - svg.viewport_dimensions() + pub fn create_cache(&self, device: &wgpu::Device) -> Cache { + Cache::new(device, self.backend, self.texture_layout.clone()) } pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - images: &[layer::Image], + belt: &mut wgpu::util::StagingBelt, + cache: &mut Cache, + images: &Batch, transformation: Transformation, - _scale: f32, + scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "PREPARE").entered(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "DRAW").entered(); + let transformation = transformation * Transformation::scale(scale); let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); - #[cfg(feature = "image")] - let mut raster_cache = self.raster_cache.borrow_mut(); - - #[cfg(feature = "svg")] - let mut vector_cache = self.vector_cache.borrow_mut(); - for image in images { match &image { #[cfg(feature = "image")] - layer::Image::Raster { + Image::Raster { handle, filter_method, bounds, + rotation, + opacity, } => { - if let Some(atlas_entry) = raster_cache.upload( - device, - encoder, - handle, - &mut self.texture_atlas, - ) { + if let Some(atlas_entry) = + cache.upload_raster(device, encoder, handle) + { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], + f32::from(*rotation), + *opacity, atlas_entry, match filter_method { - image::FilterMethod::Nearest => { + crate::core::image::FilterMethod::Nearest => { nearest_instances } - image::FilterMethod::Linear => linear_instances, + crate::core::image::FilterMethod::Linear => { + linear_instances + } }, ); } } #[cfg(not(feature = "image"))] - layer::Image::Raster { .. } => {} + Image::Raster { .. } => {} #[cfg(feature = "svg")] - layer::Image::Vector { + Image::Vector { handle, color, bounds, + rotation, + opacity, } => { let size = [bounds.width, bounds.height]; - if let Some(atlas_entry) = vector_cache.upload( - device, - encoder, - handle, - *color, - size, - _scale, - &mut self.texture_atlas, + if let Some(atlas_entry) = cache.upload_vector( + device, encoder, handle, *color, size, scale, ) { add_instances( [bounds.x, bounds.y], size, + f32::from(*rotation), + *opacity, atlas_entry, nearest_instances, ); } } #[cfg(not(feature = "svg"))] - layer::Image::Vector { .. } => {} + Image::Vector { .. } => {} } } @@ -460,26 +282,6 @@ impl Pipeline { return; } - let texture_version = self.texture_atlas.layer_count(); - - if self.texture_version != texture_version { - log::info!("Atlas has grown. Recreating bind group..."); - - self.texture = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &self.texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - self.texture_atlas.view(), - ), - }], - }); - - self.texture_version = texture_version; - } - if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new( device, @@ -493,7 +295,8 @@ impl Pipeline { layer.prepare( device, - queue, + encoder, + belt, nearest_instances, linear_instances, transformation, @@ -504,6 +307,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + cache: &'a Cache, layer: usize, bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, @@ -518,28 +322,173 @@ impl Pipeline { bounds.height, ); - render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_bind_group(1, cache.bind_group(), &[]); layer.render(render_pass); } } pub fn end_frame(&mut self) { - #[cfg(feature = "image")] - self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); - - #[cfg(feature = "svg")] - self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); - self.prepare_layer = 0; } } +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + nearest: Data, + linear: Data, +} + +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + let uniforms = Uniforms { + transform: transformation.into(), + }; + + let bytes = bytemuck::bytes_of(&uniforms); + + belt.write_buffer( + encoder, + &self.uniforms, + 0, + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); + + self.nearest + .upload(device, encoder, belt, nearest_instances); + + self.linear.upload(device, encoder, belt, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer<Instance>, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + constants, + instances, + instance_count: 0, + } + } + + fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Instance], + ) { + self.instance_count = instances.len(); + + if self.instance_count == 0 { + return; + } + + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + if self.instance_count == 0 { + return; + } + + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(0, self.instances.slice(..)); + + render_pass.draw(0..6, 0..self.instance_count as u32); + } +} + #[repr(C)] #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], + _center: [f32; 2], _size: [f32; 2], + _rotation: f32, + _opacity: f32, _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -558,12 +507,27 @@ struct Uniforms { fn add_instances( image_position: [f32; 2], image_size: [f32; 2], + rotation: f32, + opacity: f32, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { + let center = [ + image_position[0] + image_size[0] / 2.0, + image_position[1] + image_size[1] / 2.0, + ]; + match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image_position, image_size, allocation, instances); + add_instance( + image_position, + center, + image_size, + rotation, + opacity, + allocation, + instances, + ); } atlas::Entry::Fragmented { fragments, size } => { let scaling_x = image_size[0] / size.width as f32; @@ -589,7 +553,10 @@ fn add_instances( fragment_height as f32 * scaling_y, ]; - add_instance(position, size, allocation, instances); + add_instance( + position, center, size, rotation, opacity, allocation, + instances, + ); } } } @@ -598,7 +565,10 @@ fn add_instances( #[inline] fn add_instance( position: [f32; 2], + center: [f32; 2], size: [f32; 2], + rotation: f32, + opacity: f32, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -608,7 +578,10 @@ fn add_instance( let instance = Instance { _position: position, + _center: center, _size: size, + _rotation: rotation, + _opacity: opacity, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs new file mode 100644 index 00000000..c06d56be --- /dev/null +++ b/wgpu/src/image/null.rs @@ -0,0 +1,10 @@ +pub use crate::graphics::Image; + +#[derive(Debug, Default)] +pub struct Batch; + +impl Batch { + pub fn push(&mut self, _image: Image) {} + + pub fn clear(&mut self) {} +} diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index a6cba76a..4d3c3125 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -4,13 +4,13 @@ use crate::graphics; use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; /// Entry in cache corresponding to an image handle #[derive(Debug)] pub enum Memory { /// Image data on host - Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>), + Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>), /// Storage entry Device(atlas::Entry), /// Image not found @@ -38,8 +38,9 @@ impl Memory { /// Caches image raster data #[derive(Debug, Default)] pub struct Cache { - map: HashMap<u64, Memory>, - hits: HashSet<u64>, + map: FxHashMap<image::Id, Memory>, + hits: FxHashSet<image::Id>, + should_trim: bool, } impl Cache { @@ -50,11 +51,13 @@ impl Cache { } let memory = match graphics::image::load(handle) { - Ok(image) => Memory::Host(image.to_rgba8()), + Ok(image) => Memory::Host(image), Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, Err(_) => Memory::Invalid, }; + self.should_trim = true; + self.insert(handle, memory); self.get(handle).unwrap() } @@ -86,6 +89,11 @@ impl Cache { /// Trim cache misses from cache pub fn trim(&mut self, atlas: &mut Atlas) { + // Only trim if new entries have landed in the `Cache` + if !self.should_trim { + return; + } + let hits = &self.hits; self.map.retain(|k, memory| { @@ -101,6 +109,7 @@ impl Cache { }); self.hits.clear(); + self.should_trim = false; } fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index d9be50d7..c6d829af 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -5,7 +5,7 @@ use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; use resvg::usvg::{self, TreeTextToPath}; -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; /// Entry in cache corresponding to an svg handle @@ -33,10 +33,11 @@ impl Svg { /// Caches svg vector and raster data #[derive(Debug, Default)] pub struct Cache { - svgs: HashMap<u64, Svg>, - rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, - svg_hits: HashSet<u64>, - rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>, + svgs: FxHashMap<u64, Svg>, + rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, + svg_hits: FxHashSet<u64>, + rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, + should_trim: bool, } type ColorFilter = Option<[u8; 4]>; @@ -76,6 +77,8 @@ impl Cache { } } + self.should_trim = true; + let _ = self.svgs.insert(handle.id(), svg); self.svgs.get(&handle.id()).unwrap() } @@ -176,6 +179,10 @@ impl Cache { /// Load svg and upload raster data pub fn trim(&mut self, atlas: &mut Atlas) { + if !self.should_trim { + return; + } + let svg_hits = &self.svg_hits; let rasterized_hits = &self.rasterized_hits; @@ -191,6 +198,7 @@ impl Cache { }); self.svg_hits.clear(); self.rasterized_hits.clear(); + self.should_trim = false; } } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index cc767c25..9551311d 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,343 +1,302 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod pipeline; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use pipeline::Pipeline; -pub use text::Text; - -use crate::core; -use crate::core::alignment; use crate::core::{ - Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector, + renderer, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics; use crate::graphics::color; -use crate::graphics::Viewport; +use crate::graphics::layer; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Mesh; +use crate::image::{self, Image}; use crate::primitive::{self, Primitive}; use crate::quad::{self, Quad}; +use crate::text::{self, Text}; +use crate::triangle; + +pub type Stack = layer::Stack<Layer>; -/// A group of primitives that should be clipped together. #[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. +pub struct Layer { pub bounds: Rectangle, - - /// The quads of the [`Layer`]. pub quads: quad::Batch, - - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec<Mesh<'a>>, - - /// The text of the [`Layer`]. - pub text: Vec<Text<'a>>, - - /// The images of the [`Layer`]. - pub images: Vec<Image>, - - /// The custom pipelines of this [`Layer`]. - pub pipelines: Vec<Pipeline>, + pub triangles: triangle::Batch, + pub primitives: primitive::Batch, + pub text: text::Batch, + pub images: image::Batch, + pending_meshes: Vec<Mesh>, + pending_text: Vec<Text>, } -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { +impl Layer { + pub fn draw_quad( + &mut self, + quad: renderer::Quad, + background: Background, + transformation: Transformation, + ) { + let bounds = quad.bounds * transformation; + + let quad = Quad { + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + border_color: color::pack(quad.border.color), + border_radius: quad.border.radius.into(), + border_width: quad.border.width, + shadow_color: color::pack(quad.shadow.color), + shadow_offset: quad.shadow.offset.into(), + shadow_blur_radius: quad.shadow.blur_radius, + }; + + self.quads.add(quad, &background); + } + + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(paragraph); + } + + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(editor); + } + + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.pending_text.push(text); + } + + pub fn draw_image( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: crate::core::svg::Handle, + color: Option<Color>, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(svg); + } + + pub fn draw_mesh( + &mut self, + mut mesh: Mesh, + transformation: Transformation, + ) { + match &mut mesh { + Mesh::Solid { + transformation: local_transformation, + .. + } + | Mesh::Gradient { + transformation: local_transformation, + .. + } => { + *local_transformation = *local_transformation * transformation; + } + } + + self.pending_meshes.push(mesh); + } + + pub fn draw_mesh_group( + &mut self, + meshes: Vec<Mesh>, + transformation: Transformation, + ) { + self.flush_meshes(); + + self.triangles.push(triangle::Item::Group { + meshes, + transformation, + }); + } + + pub fn draw_mesh_cache( + &mut self, + cache: triangle::Cache, + transformation: Transformation, + ) { + self.flush_meshes(); + + self.triangles.push(triangle::Item::Cached { + cache, + transformation, + }); + } + + pub fn draw_text_group( + &mut self, + text: Vec<Text>, + transformation: Transformation, + ) { + self.flush_text(); + + self.text.push(text::Item::Group { + text, + transformation, + }); + } + + pub fn draw_text_cache( + &mut self, + cache: text::Cache, + transformation: Transformation, + ) { + self.flush_text(); + + self.text.push(text::Item::Cached { + cache, + transformation, + }); + } + + pub fn draw_primitive( + &mut self, + bounds: Rectangle, + primitive: Box<dyn Primitive>, + transformation: Transformation, + ) { + let bounds = bounds * transformation; + + self.primitives + .push(primitive::Instance { bounds, primitive }); + } + + fn flush_meshes(&mut self) { + if !self.pending_meshes.is_empty() { + self.triangles.push(triangle::Item::Group { + transformation: Transformation::IDENTITY, + meshes: self.pending_meshes.drain(..).collect(), + }); + } + } + + fn flush_text(&mut self) { + if !self.pending_text.is_empty() { + self.text.push(text::Item::Group { + transformation: Transformation::IDENTITY, + text: self.pending_text.drain(..).collect(), + }); + } + } +} + +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { Self { bounds, + ..Self::default() + } + } + + fn flush(&mut self) { + self.flush_meshes(); + self.flush_text(); + } + + fn resize(&mut self, bounds: Rectangle) { + self.bounds = bounds; + } + + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; + + self.quads.clear(); + self.triangles.clear(); + self.primitives.clear(); + self.text.clear(); + self.images.clear(); + self.pending_meshes.clear(); + self.pending_text.clear(); + } +} + +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, quads: quad::Batch::default(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - pipelines: Vec::new(), - } - } - - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = text::Cached { - content: line.as_ref(), - bounds: Rectangle::new( - Point::new(11.0, 11.0 + 25.0 * i as f32), - Size::INFINITY, - ), - color: Color::new(0.9, 0.9, 0.9, 1.0), - size: Pixels(20.0), - line_height: core::text::LineHeight::default(), - font: Font::MONOSPACE, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }; - - overlay.text.push(Text::Cached(text.clone())); - - overlay.text.push(Text::Cached(text::Cached { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - })); - } - - overlay - } - - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec<Self> { - let first_layer = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - let mut layers = vec![first_layer]; - - for primitive in primitives { - Self::process_primitive( - &mut layers, - Transformation::IDENTITY, - primitive, - 0, - ); - } - - layers - } - - fn process_primitive( - layers: &mut Vec<Self>, - transformation: Transformation, - primitive: &'a Primitive, - current_layer: usize, - ) { - match primitive { - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Paragraph { - paragraph: paragraph.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Editor { - editor: editor.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Text { - content, - bounds, - size, - line_height, - color, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Cached(text::Cached { - content, - bounds: *bounds + transformation.translation(), - size: *size * transformation.scale_factor(), - line_height: *line_height, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - shaping: *shaping, - clip_bounds: *clip_bounds * transformation, - })); - } - graphics::Primitive::RawText(raw) => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Raw { - raw: raw.clone(), - transformation, - }); - } - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - let layer = &mut layers[current_layer]; - let bounds = *bounds * transformation; - - let quad = Quad { - position: [bounds.x, bounds.y], - size: [bounds.width, bounds.height], - border_color: color::pack(border.color), - border_radius: border.radius.into(), - border_width: border.width, - shadow_color: shadow.color.into_linear(), - shadow_offset: shadow.offset.into(), - shadow_blur_radius: shadow.blur_radius, - }; - - layer.quads.add(quad, background); - } - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Raster { - handle: handle.clone(), - filter_method: *filter_method, - bounds: *bounds * transformation, - }); - } - Primitive::Svg { - handle, - color, - bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds * transformation, - }); - } - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - transformation, - primitive, - current_layer, - ); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&translated_bounds) - { - let clip_layer = Layer::new(clip_bounds); - layers.push(clip_layer); - - Self::process_primitive( - layers, - transformation, - content, - layers.len() - 1, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - Self::process_primitive( - layers, - transformation * *new_transformation, - content, - current_layer, - ); - } - Primitive::Cache { content } => { - Self::process_primitive( - layers, - transformation, - content, - current_layer, - ); - } - Primitive::Custom(custom) => match custom { - primitive::Custom::Mesh(mesh) => match mesh { - graphics::Mesh::Solid { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Solid { - transformation, - buffers, - clip_bounds, - }); - } - } - graphics::Mesh::Gradient { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Gradient { - transformation, - buffers, - clip_bounds, - }); - } - } - }, - primitive::Custom::Pipeline(pipeline) => { - let layer = &mut layers[current_layer]; - let bounds = pipeline.bounds * transformation; - - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.pipelines.push(Pipeline { - bounds, - viewport: clip_bounds, - primitive: pipeline.primitive.clone(), - }); - } - } - }, + triangles: triangle::Batch::default(), + primitives: primitive::Batch::default(), + text: text::Batch::default(), + images: image::Batch::default(), + pending_meshes: Vec::new(), + pending_text: Vec::new(), } } } diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs deleted file mode 100644 index facbe192..00000000 --- a/wgpu/src/layer/image.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; - -/// A raster or vector image. -#[derive(Debug, Clone)] -pub enum Image { - /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, - - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs deleted file mode 100644 index 5ed7c654..00000000 --- a/wgpu/src/layer/mesh.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! A collection of triangle primitives. -use crate::core::{Rectangle, Transformation}; -use crate::graphics::mesh; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::SolidVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::GradientVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn transformation(&self) -> Transformation { - match self { - Self::Solid { transformation, .. } - | Self::Gradient { transformation, .. } => *transformation, - } - } - - /// Returns the indices of the [`Mesh`]. - pub fn indices(&self) -> &[u32] { - match self { - Self::Solid { buffers, .. } => &buffers.indices, - Self::Gradient { buffers, .. } => &buffers.indices, - } - } - - /// Returns the clip bounds of the [`Mesh`]. - pub fn clip_bounds(&self) -> Rectangle<f32> { - match self { - Self::Solid { clip_bounds, .. } - | Self::Gradient { clip_bounds, .. } => *clip_bounds, - } - } -} - -/// The result of counting the attributes of a set of meshes. -#[derive(Debug, Clone, Copy, Default)] -pub struct AttributeCount { - /// The total amount of solid vertices. - pub solid_vertices: usize, - - /// The total amount of solid meshes. - pub solids: usize, - - /// The total amount of gradient vertices. - pub gradient_vertices: usize, - - /// The total amount of gradient meshes. - pub gradients: usize, - - /// The total amount of indices. - pub indices: usize, -} - -/// Returns the number of total vertices & total indices of all [`Mesh`]es. -pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { - meshes - .iter() - .fold(AttributeCount::default(), |mut count, mesh| { - match mesh { - Mesh::Solid { buffers, .. } => { - count.solids += 1; - count.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradients += 1; - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs deleted file mode 100644 index 6dfe6750..00000000 --- a/wgpu/src/layer/pipeline.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::core::Rectangle; -use crate::primitive::pipeline::Primitive; - -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The viewport of the [`Pipeline`]. - pub viewport: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc<dyn Primitive>, -} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs deleted file mode 100644 index b3a00130..00000000 --- a/wgpu/src/layer/text.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::core::alignment; -use crate::core::text; -use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; -use crate::graphics; -use crate::graphics::text::editor; -use crate::graphics::text::paragraph; - -/// A text primitive. -#[derive(Debug, Clone)] -pub enum Text<'a> { - /// A paragraph. - #[allow(missing_docs)] - Paragraph { - paragraph: paragraph::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// An editor. - #[allow(missing_docs)] - Editor { - editor: editor::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// Some cached text. - Cached(Cached<'a>), - /// Some raw text. - #[allow(missing_docs)] - Raw { - raw: graphics::text::Raw, - transformation: Transformation, - }, -} - -#[derive(Debug, Clone)] -pub struct Cached<'a> { - /// The content of the [`Text`]. - pub content: &'a str, - - /// The layout bounds of the [`Text`]. - pub bounds: Rectangle, - - /// The color of the [`Text`], in __linear RGB_. - pub color: Color, - - /// The size of the [`Text`] in logical pixels. - pub size: Pixels, - - /// The line height of the [`Text`]. - pub line_height: text::LineHeight, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, - - /// The shaping strategy of the text. - pub shaping: text::Shaping, - - /// The clip bounds of the text. - pub clip_bounds: Rectangle, -} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b00e5c3c..095e1f1b 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -20,15 +20,8 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unsafe_code, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(missing_docs)] pub mod layer; pub mod primitive; pub mod settings; @@ -37,13 +30,21 @@ pub mod window; #[cfg(feature = "geometry")] pub mod geometry; -mod backend; mod buffer; mod color; +mod engine; mod quad; mod text; mod triangle; +#[cfg(any(feature = "image", feature = "svg"))] +#[path = "image/mod.rs"] +mod image; + +#[cfg(not(any(feature = "image", feature = "svg")))] +#[path = "image/null.rs"] +mod image; + use buffer::Buffer; pub use iced_graphics as graphics; @@ -51,16 +52,525 @@ pub use iced_graphics::core; pub use wgpu; -pub use backend::Backend; +pub use engine::Engine; pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; -#[cfg(any(feature = "image", feature = "svg"))] -mod image; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +#[allow(missing_debug_implementations)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + + triangle_storage: triangle::Storage, + text_storage: text::Storage, + text_viewport: text::Viewport, + + // TODO: Centralize all the image feature handling + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell<image::Cache>, +} + +impl Renderer { + pub fn new( + device: &wgpu::Device, + engine: &Engine, + default_font: Font, + default_text_size: Pixels, + ) -> Self { + Self { + default_font, + default_text_size, + layers: layer::Stack::new(), + + triangle_storage: triangle::Storage::new(), + text_storage: text::Storage::new(), + text_viewport: engine.text_pipeline.create_viewport(device), + + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell::new( + engine.create_image_cache(device), + ), + } + } + + pub fn present( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + clear_color: Option<Color>, + format: wgpu::TextureFormat, + frame: &wgpu::TextureView, + viewport: &Viewport, + ) { + self.prepare(engine, device, queue, format, encoder, viewport); + self.render(engine, encoder, frame, clear_color, viewport); + + self.triangle_storage.trim(); + self.text_storage.trim(); + + #[cfg(any(feature = "svg", feature = "image"))] + self.image_cache.borrow_mut().trim(); + } + + fn prepare( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + _format: wgpu::TextureFormat, + encoder: &mut wgpu::CommandEncoder, + viewport: &Viewport, + ) { + let scale_factor = viewport.scale_factor() as f32; + + self.text_viewport.update(queue, viewport.physical_size()); + + for layer in self.layers.iter_mut() { + if !layer.quads.is_empty() { + engine.quad_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.quads, + viewport.projection(), + scale_factor, + ); + } + + if !layer.triangles.is_empty() { + engine.triangle_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.triangle_storage, + &layer.triangles, + Transformation::scale(scale_factor), + viewport.physical_size(), + ); + } + + if !layer.primitives.is_empty() { + for instance in &layer.primitives { + instance.primitive.prepare( + device, + queue, + engine.format, + &mut engine.primitive_storage, + &instance.bounds, + viewport, + ); + } + } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + &self.text_viewport, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.image_cache.borrow_mut(), + &layer.images, + viewport.projection(), + scale_factor, + ); + } + } + } + + fn render( + &mut self, + engine: &mut Engine, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + clear_color: Option<Color>, + viewport: &Viewport, + ) { + use std::mem::ManuallyDrop; + + let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: match clear_color { + Some(background_color) => wgpu::LoadOp::Clear({ + let [r, g, b, a] = + graphics::color::pack(background_color) + .components(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + None => wgpu::LoadOp::Load, + }, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + + let mut quad_layer = 0; + let mut mesh_layer = 0; + let mut text_layer = 0; + + #[cfg(any(feature = "svg", feature = "image"))] + let mut image_layer = 0; + #[cfg(any(feature = "svg", feature = "image"))] + let image_cache = self.image_cache.borrow(); + + let scale_factor = viewport.scale_factor() as f32; + let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( + viewport.physical_size(), + )); + + let scale = Transformation::scale(scale_factor); + + for layer in self.layers.iter() { + let Some(physical_bounds) = + physical_bounds.intersection(&(layer.bounds * scale)) + else { + continue; + }; + + let Some(scissor_rect) = physical_bounds.snap() else { + continue; + }; + + if !layer.quads.is_empty() { + engine.quad_pipeline.render( + quad_layer, + scissor_rect, + &layer.quads, + &mut render_pass, + ); + + quad_layer += 1; + } + + if !layer.triangles.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + mesh_layer += engine.triangle_pipeline.render( + encoder, + frame, + &self.triangle_storage, + mesh_layer, + &layer.triangles, + physical_bounds, + scale, + ); + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.primitives.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + for instance in &layer.primitives { + if let Some(clip_bounds) = (instance.bounds * scale) + .intersection(&physical_bounds) + .and_then(Rectangle::snap) + { + instance.primitive.render( + encoder, + &engine.primitive_storage, + frame, + &clip_bounds, + ); + } + } + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_viewport, + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.render( + &image_cache, + image_layer, + scissor_rect, + &mut render_pass, + ); + + image_layer += 1; + } + } + + let _ = ManuallyDrop::into_inner(render_pass); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: core::renderer::Quad, + background: impl Into<Background>, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size<u32> { + self.image_cache.borrow_mut().measure_image(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> { + self.image_cache.borrow_mut().measure_svg(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color_filter: Option<Color>, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_mesh(mesh, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { meshes, text } => { + layer.draw_mesh_group(meshes, transformation); + layer.draw_text_group(text, transformation); + } + Geometry::Cached(cache) => { + if let Some(meshes) = cache.meshes { + layer.draw_mesh_cache(meshes, transformation); + } + + if let Some(text) = cache.text { + layer.draw_text_cache(text, transformation); + } + } + } + } +} + +impl primitive::Renderer for Renderer { + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_primitive(bounds, Box::new(primitive), transformation); + } +} + +impl graphics::compositor::Default for crate::Renderer { + type Compositor = window::Compositor; +} diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index fff927ea..8641f27a 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,30 +1,95 @@ -//! Draw using different graphical primitives. -pub mod pipeline; - -pub use pipeline::Pipeline; - -use crate::core::Rectangle; -use crate::graphics::{Damage, Mesh}; +//! Draw custom primitives. +use crate::core::{self, Rectangle}; +use crate::graphics::Viewport; +use rustc_hash::FxHashMap; +use std::any::{Any, TypeId}; use std::fmt::Debug; -/// The graphical primitives supported by `iced_wgpu`. -pub type Primitive = crate::graphics::Primitive<Custom>; +/// A batch of primitives. +pub type Batch = Vec<Instance>; -/// The custom primitives supported by `iced_wgpu`. -#[derive(Debug, Clone, PartialEq)] -pub enum Custom { - /// A mesh primitive. - Mesh(Mesh), - /// A custom pipeline primitive. - Pipeline(Pipeline), +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + storage: &mut Storage, + bounds: &Rectangle, + viewport: &Viewport, + ); + + /// Renders the [`Primitive`]. + fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + storage: &Storage, + target: &wgpu::TextureView, + clip_bounds: &Rectangle<u32>, + ); } -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Mesh(mesh) => mesh.bounds(), - Self::Pipeline(pipeline) => pipeline.bounds, +#[derive(Debug)] +/// An instance of a specific [`Primitive`]. +pub struct Instance { + /// The bounds of the [`Instance`]. + pub bounds: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Box<dyn Primitive>, +} + +impl Instance { + /// Creates a new [`Instance`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Instance { + bounds, + primitive: Box::new(primitive), } } } + +/// A renderer than can draw custom primitives. +pub trait Renderer: core::Renderer { + /// Draws a custom primitive. + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive); +} + +/// Stores custom, user-provided types. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: FxHashMap<TypeId, Box<dyn Any + Send>>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a type `T`. + pub fn has<T: 'static>(&self) -> bool { + self.pipelines.contains_key(&TypeId::of::<T>()) + } + + /// Inserts the data `T` in to [`Storage`]. + pub fn store<T: 'static + Send>(&mut self, data: T) { + let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(data)); + } + + /// Returns a reference to the data with type `T` if it exists in [`Storage`]. + pub fn get<T: 'static>(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_ref::<T>() + .expect("Value with this type does not exist in Storage.") + }) + } + + /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`]. + pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_mut::<T>() + .expect("Value with this type does not exist in Storage.") + }) + } +} diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs deleted file mode 100644 index c6b7c5e2..00000000 --- a/wgpu/src/primitive/pipeline.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Draw primitives using custom pipelines. -use crate::core::{Rectangle, Size}; - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc<dyn Primitive>, -} - -impl Pipeline { - /// Creates a new [`Pipeline`] with the given [`Primitive`]. - pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { - Pipeline { - bounds, - primitive: Arc::new(primitive), - } - } -} - -impl PartialEq for Pipeline { - fn eq(&self, other: &Self) -> bool { - self.primitive.type_id() == other.primitive.type_id() - } -} - -/// A set of methods which allows a [`Primitive`] to be rendered. -pub trait Primitive: Debug + Send + Sync + 'static { - /// Processes the [`Primitive`], allowing for GPU buffer allocation. - fn prepare( - &self, - format: wgpu::TextureFormat, - device: &wgpu::Device, - queue: &wgpu::Queue, - bounds: Rectangle, - target_size: Size<u32>, - scale_factor: f32, - storage: &mut Storage, - ); - - /// Renders the [`Primitive`]. - fn render( - &self, - storage: &Storage, - target: &wgpu::TextureView, - target_size: Size<u32>, - viewport: Rectangle<u32>, - encoder: &mut wgpu::CommandEncoder, - ); -} - -/// A renderer than can draw custom pipeline primitives. -pub trait Renderer: crate::core::Renderer { - /// Draws a custom pipeline primitive. - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ); -} - -impl Renderer for crate::Renderer { - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ) { - self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline( - Pipeline::new(bounds, primitive), - ))); - } -} - -/// Stores custom, user-provided pipelines. -#[derive(Default, Debug)] -pub struct Storage { - pipelines: HashMap<TypeId, Box<dyn Any + Send>>, -} - -impl Storage { - /// Returns `true` if `Storage` contains a pipeline with type `T`. - pub fn has<T: 'static>(&self) -> bool { - self.pipelines.get(&TypeId::of::<T>()).is_some() - } - - /// Inserts the pipeline `T` in to [`Storage`]. - pub fn store<T: 'static + Send>(&mut self, pipeline: T) { - let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); - } - - /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. - pub fn get<T: 'static>(&self) -> Option<&T> { - self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_ref::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } - - /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. - pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { - self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_mut::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } -} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index b932f54f..de432d2f 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable}; use std::mem; -#[cfg(feature = "tracing")] -use tracing::info_span; - const INITIAL_INSTANCES: usize = 2_000; +/// The properties of a quad. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: color::Packed, + + /// The border radii of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The shadow color of the [`Quad`]. + pub shadow_color: color::Packed, + + /// The shadow offset of the [`Quad`]. + pub shadow_offset: [f32; 2], + + /// The shadow blur radius of the [`Quad`]. + pub shadow_blur_radius: f32, +} + #[derive(Debug)] pub struct Pipeline { solid: solid::Pipeline, @@ -57,7 +83,8 @@ impl Pipeline { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, quads: &Batch, transformation: Transformation, scale: f32, @@ -67,7 +94,7 @@ impl Pipeline { } let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, quads, transformation, scale); + layer.prepare(device, encoder, belt, quads, transformation, scale); self.prepare_layer += 1; } @@ -123,7 +150,7 @@ impl Pipeline { } #[derive(Debug)] -struct Layer { +pub struct Layer { constants: wgpu::BindGroup, constants_buffer: wgpu::Buffer, solid: solid::Layer, @@ -162,56 +189,46 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, quads: &Batch, transformation: Transformation, scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "PREPARE").entered(); + self.update(device, encoder, belt, transformation, scale); + if !quads.solids.is_empty() { + self.solid.prepare(device, encoder, belt, &quads.solids); + } + + if !quads.gradients.is_empty() { + self.gradient + .prepare(device, encoder, belt, &quads.gradients); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + transformation: Transformation, + scale: f32, + ) { let uniforms = Uniforms::new(transformation, scale); + let bytes = bytemuck::bytes_of(&uniforms); - queue.write_buffer( + belt.write_buffer( + encoder, &self.constants_buffer, 0, - bytemuck::bytes_of(&uniforms), - ); - - self.solid.prepare(device, queue, &quads.solids); - self.gradient.prepare(device, queue, &quads.gradients); + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); } } -/// The properties of a quad. -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: color::Packed, - - /// The border radii of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The shadow color of the [`Quad`]. - pub shadow_color: [f32; 4], - - /// The shadow offset of the [`Quad`]. - pub shadow_offset: [f32; 2], - - /// The shadow blur radius of the [`Quad`]. - pub shadow_blur_radius: f32, -} - /// A group of [`Quad`]s rendered together. #[derive(Default, Debug)] pub struct Batch { @@ -221,10 +238,13 @@ pub struct Batch { /// The gradient quads of the [`Layer`]. gradients: Vec<Gradient>, - /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count. - order: Vec<(Kind, usize)>, + /// The quad order of the [`Layer`]. + order: Order, } +/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count. +type Order = Vec<(Kind, usize)>; + impl Batch { /// Returns true if there are no quads of any type in [`Quads`]. pub fn is_empty(&self) -> bool { @@ -264,6 +284,12 @@ impl Batch { } } } + + pub fn clear(&mut self) { + self.solids.clear(); + self.gradients.clear(); + self.order.clear(); + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 560fcad2..5b32c52a 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -46,11 +46,12 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, instances: &[Gradient], ) { let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); + let _ = self.instances.write(device, encoder, belt, 0, instances); self.instance_count = instances.len(); } diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 771eee34..1cead367 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -40,11 +40,12 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, instances: &[Solid], ) { let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); + let _ = self.instances.write(device, encoder, belt, 0, instances); self.instance_count = instances.len(); } diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index c9338fec..b3c3cf6a 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,19 +1,19 @@ //! Configure a renderer. use crate::core::{Font, Pixels}; -use crate::graphics::Antialiasing; +use crate::graphics::{self, Antialiasing}; -/// The settings of a [`Backend`]. +/// The settings of a [`Renderer`]. /// -/// [`Backend`]: crate::Backend +/// [`Renderer`]: crate::Renderer #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { - /// The present mode of the [`Backend`]. + /// The present mode of the [`Renderer`]. /// - /// [`Backend`]: crate::Backend + /// [`Renderer`]: crate::Renderer pub present_mode: wgpu::PresentMode, - /// The internal graphics backend to use. - pub internal_backend: wgpu::Backends, + /// The graphics backends to use. + pub backends: wgpu::Backends, /// The default [`Font`] to use. pub default_font: Font, @@ -29,38 +29,51 @@ pub struct Settings { pub antialiasing: Option<Antialiasing>, } -impl Settings { - /// Creates new [`Settings`] using environment configuration. - /// - /// Specifically: - /// - /// - The `internal_backend` can be configured using the `WGPU_BACKEND` - /// environment variable. If the variable is not set, the primary backend - /// will be used. The following values are allowed: - /// - `vulkan` - /// - `metal` - /// - `dx12` - /// - `dx11` - /// - `gl` - /// - `webgpu` - /// - `primary` - pub fn from_env() -> Self { - Settings { - internal_backend: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::all()), - ..Self::default() - } - } -} - impl Default for Settings { fn default() -> Settings { Settings { present_mode: wgpu::PresentMode::AutoVsync, - internal_backend: wgpu::Backends::all(), + backends: wgpu::Backends::all(), default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: None, } } } + +impl From<graphics::Settings> for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..Settings::default() + } + } +} + +/// Obtains a [`wgpu::PresentMode`] from the current environment +/// configuration, if set. +/// +/// The value returned by this function can be changed by setting +/// the `ICED_PRESENT_MODE` env variable. The possible values are: +/// +/// - `vsync` → [`wgpu::PresentMode::AutoVsync`] +/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`] +/// - `immediate` → [`wgpu::PresentMode::Immediate`] +/// - `fifo` → [`wgpu::PresentMode::Fifo`] +/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`] +/// - `mailbox` → [`wgpu::PresentMode::Mailbox`] +pub fn present_mode_from_env() -> Option<wgpu::PresentMode> { + let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?; + + match present_mode.to_lowercase().as_str() { + "vsync" => Some(wgpu::PresentMode::AutoVsync), + "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync), + "immediate" => Some(wgpu::PresentMode::Immediate), + "fifo" => Some(wgpu::PresentMode::Fifo), + "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed), + "mailbox" => Some(wgpu::PresentMode::Mailbox), + _ => None, + } +} diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl index c2ea223f..d7633808 100644 --- a/wgpu/src/shader/blit.wgsl +++ b/wgpu/src/shader/blit.wgsl @@ -1,22 +1,14 @@ -var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>( - vec2<f32>(-1.0, 1.0), - vec2<f32>(-1.0, -1.0), - vec2<f32>(1.0, -1.0), - vec2<f32>(-1.0, 1.0), - vec2<f32>(1.0, 1.0), - vec2<f32>(1.0, -1.0) -); - var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>( vec2<f32>(0.0, 0.0), - vec2<f32>(0.0, 1.0), + vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 0.0), - vec2<f32>(1.0, 0.0), + vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 1.0) ); @group(0) @binding(0) var u_sampler: sampler; +@group(0) @binding(1) var<uniform> u_ratio: vec2<f32>; @group(1) @binding(0) var u_texture: texture_2d<f32>; struct VertexInput { @@ -30,9 +22,11 @@ struct VertexOutput { @vertex fn vs_main(input: VertexInput) -> VertexOutput { + let uv = uvs[input.vertex_index]; + var out: VertexOutput; - out.uv = uvs[input.vertex_index]; - out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0); + out.uv = uv * u_ratio; + out.position = vec4<f32>(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); return out; } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 7b2e5238..accefc17 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,40 +9,55 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2<f32>, - @location(1) scale: vec2<f32>, - @location(2) atlas_pos: vec2<f32>, - @location(3) atlas_scale: vec2<f32>, - @location(4) layer: i32, + @location(1) center: vec2<f32>, + @location(2) scale: vec2<f32>, + @location(3) rotation: f32, + @location(4) opacity: f32, + @location(5) atlas_pos: vec2<f32>, + @location(6) atlas_scale: vec2<f32>, + @location(7) layer: i32, } struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>, @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. + @location(2) opacity: f32, } @vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; - let v_pos = vertex_position(input.vertex_index); + // Generate a vertex position in the range [0, 1] from the vertex index. + var v_pos = vertex_position(input.vertex_index); + // Map the vertex position to the atlas texture. out.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); + out.opacity = input.opacity; - var transform: mat4x4<f32> = mat4x4<f32>( - vec4<f32>(input.scale.x, 0.0, 0.0, 0.0), - vec4<f32>(0.0, input.scale.y, 0.0, 0.0), + // Calculate the vertex position and move the center to the origin + v_pos = input.pos + v_pos * input.scale - input.center; + + // Apply the rotation around the center of the image + let cos_rot = cos(input.rotation); + let sin_rot = sin(input.rotation); + let rotate = mat4x4<f32>( + vec4<f32>(cos_rot, sin_rot, 0.0, 0.0), + vec4<f32>(-sin_rot, cos_rot, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(input.pos, 0.0, 1.0) + vec4<f32>(0.0, 0.0, 0.0, 1.0) ); - out.position = globals.transform * transform * vec4<f32>(v_pos, 0.0, 1.0); + // Calculate the final position of the vertex + out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); + // Sample the texture at the given UV coordinate and layer. + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4<f32>(1.0, 1.0, 1.0, input.opacity); } diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index 4de73362..a367d5e6 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/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index 1274f814..d908afbc 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -107,13 +107,19 @@ fn solid_fs_main( let quad_color = vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); if input.shadow_color.a > 0.0 { - let shadow_distance = rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, border_radius); + let shadow_radius = select_border_radius( + input.border_radius, + input.position.xy - input.shadow_offset, + (input.pos + input.scale * 0.5).xy + ); + let shadow_distance = max(rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, shadow_radius), 0.); + let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, shadow_distance); let shadow_color = input.shadow_color; - let base_color = select( + let base_color = mix( vec4<f32>(shadow_color.x, shadow_color.y, shadow_color.z, 0.0), quad_color, - quad_color.a > 0.0 + quad_color.a ); return mix(base_color, shadow_color, (1.0 - radius_alpha) * shadow_alpha); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6fa1922d..05db5f80 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,20 +1,285 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use crate::layer::Text; -use std::borrow::Cow; -use std::cell::RefCell; +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::rc::{self, Rc}; +use std::sync::atomic::{self, AtomicU64}; use std::sync::Arc; +pub use crate::graphics::Text; + +const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate +} else { + glyphon::ColorMode::Web +}; + +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + text: Vec<Text>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + group: cache::Group, + text: Rc<[Text]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if text.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + group, + text: Rc::from(text), + version: 0, + }) + } + + pub fn update(&mut self, text: Vec<Text>) { + if self.text.is_empty() && text.is_empty() { + return; + } + + self.text = Rc::from(text); + self.version += 1; + } +} + +struct Upload { + renderer: glyphon::TextRenderer, + buffer_cache: BufferCache, + transformation: Transformation, + version: usize, + group_version: usize, + text: rc::Weak<[Text]>, + _atlas: rc::Weak<()>, +} + +#[derive(Default)] +pub struct Storage { + groups: FxHashMap<cache::Group, Group>, + uploads: FxHashMap<Id, Upload>, +} + +struct Group { + atlas: glyphon::TextAtlas, + version: usize, + should_trim: bool, + handle: Rc<()>, // Keeps track of active uploads +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> { + if cache.text.is_empty() { + return None; + } + + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &glyphon::Viewport, + encoder: &mut wgpu::CommandEncoder, + format: wgpu::TextureFormat, + state: &glyphon::Cache, + cache: &Cache, + new_transformation: Transformation, + bounds: Rectangle, + ) { + let group_count = self.groups.len(); + + let group = self.groups.entry(cache.group).or_insert_with(|| { + log::debug!( + "New text atlas: {:?} (total: {})", + cache.group, + group_count + 1 + ); + + Group { + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, state, format, COLOR_MODE, + ), + version: 0, + should_trim: false, + handle: Rc::new(()), + } + }); + + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if upload.version != cache.version + || upload.group_version != group.version + || upload.transformation != new_transformation + { + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + viewport, + encoder, + &mut upload.renderer, + &mut group.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + ); + } + + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; + + upload.text = Rc::downgrade(&cache.text); + upload.version = cache.version; + upload.group_version = group.version; + upload.transformation = new_transformation; + + upload.buffer_cache.trim(); + } + } + hash_map::Entry::Vacant(entry) => { + let mut renderer = glyphon::TextRenderer::new( + &mut group.atlas, + device, + wgpu::MultisampleState::default(), + None, + ); + + let mut buffer_cache = BufferCache::new(); + + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + viewport, + encoder, + &mut renderer, + &mut group.atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, + ); + } + + let _ = entry.insert(Upload { + renderer, + buffer_cache, + transformation: new_transformation, + version: 0, + group_version: group.version, + text: Rc::downgrade(&cache.text), + _atlas: Rc::downgrade(&group.handle), + }); + + group.should_trim = cache.group.is_singleton(); + + log::debug!( + "New text upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + } + + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.text.strong_count() > 0); + + self.groups.retain(|id, group| { + let active_uploads = Rc::weak_count(&group.handle); + + if active_uploads == 0 { + log::debug!("Dropping text atlas: {id:?}"); + + return false; + } + + if group.should_trim { + log::trace!("Trimming text atlas: {id:?}"); + + group.atlas.trim(); + group.should_trim = false; + + // We only need to worry about glyph fighting + // when the atlas may be shared by multiple + // uploads. + if !id.is_singleton() { + log::debug!( + "Invalidating text atlas: {id:?} \ + (uploads: {active_uploads})" + ); + + group.version += 1; + } + } + + true + }); + } +} + +pub struct Viewport(glyphon::Viewport); + +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) { + self.0.update( + queue, + glyphon::Resolution { + width: resolution.width, + height: resolution.height, + }, + ); + } +} + #[allow(missing_debug_implementations)] pub struct Pipeline { - renderers: Vec<glyphon::TextRenderer>, + state: glyphon::Cache, + format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, + renderers: Vec<glyphon::TextRenderer>, prepare_layer: usize, - cache: RefCell<Cache>, + cache: BufferCache, } impl Pipeline { @@ -23,274 +288,102 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { + let state = glyphon::Cache::new(device); + let atlas = glyphon::TextAtlas::with_color_mode( + device, queue, &state, format, COLOR_MODE, + ); + Pipeline { + state, + format, renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, - queue, - format, - if color::GAMMA_CORRECTION { - glyphon::ColorMode::Accurate - } else { - glyphon::ColorMode::Web - }, - ), + atlas, prepare_layer: 0, - cache: RefCell::new(Cache::new()), + cache: BufferCache::new(), } } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); - - self.cache = RefCell::new(Cache::new()); - } - pub fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - sections: &[Text<'_>], + viewport: &Viewport, + encoder: &mut wgpu::CommandEncoder, + storage: &mut Storage, + batch: &Batch, layer_bounds: Rectangle, - scale_factor: f32, - target_size: Size<u32>, + layer_transformation: Transformation, ) { - if self.renderers.len() <= self.prepare_layer { - self.renderers.push(glyphon::TextRenderer::new( - &mut self.atlas, - device, - wgpu::MultisampleState::default(), - None, - )); - } + for item in batch { + match item { + Item::Group { + transformation, + text, + } => { + if self.renderers.len() <= self.prepare_layer { + self.renderers.push(glyphon::TextRenderer::new( + &mut self.atlas, + device, + wgpu::MultisampleState::default(), + None, + )); + } - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); - - let renderer = &mut self.renderers[self.prepare_layer]; - let cache = self.cache.get_mut(); - - enum Allocation { - Paragraph(Paragraph), - Editor(Editor), - Cache(cache::KeyHash), - Raw(Arc<glyphon::Buffer>), - } - - let allocations: Vec<_> = sections - .iter() - .map(|section| match section { - Text::Paragraph { paragraph, .. } => { - paragraph.upgrade().map(Allocation::Paragraph) - } - Text::Editor { editor, .. } => { - editor.upgrade().map(Allocation::Editor) - } - Text::Cached(text) => { - let (key, _) = cache.allocate( - font_system, - cache::Key { - content: text.content, - size: text.size.into(), - line_height: f32::from( - text.line_height.to_absolute(text.size), - ), - font: text.font, - bounds: Size { - width: text.bounds.width, - height: text.bounds.height, - }, - shaping: text.shaping, - }, + let renderer = &mut self.renderers[self.prepare_layer]; + let result = prepare( + device, + queue, + &viewport.0, + encoder, + renderer, + &mut self.atlas, + &mut self.cache, + text, + layer_bounds * layer_transformation, + layer_transformation * *transformation, ); - Some(Allocation::Cache(key)) + match result { + Ok(()) => { + self.prepare_layer += 1; + } + Err(glyphon::PrepareError::AtlasFull) => { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + } + } } - Text::Raw { raw, .. } => { - raw.buffer.upgrade().map(Allocation::Raw) - } - }) - .collect(); - - let layer_bounds = layer_bounds * scale_factor; - - let text_areas = sections.iter().zip(allocations.iter()).filter_map( - |(section, allocation)| { - let ( - buffer, - bounds, - horizontal_alignment, - vertical_alignment, - color, - clip_bounds, + Item::Cached { transformation, - ) = match section { - Text::Paragraph { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Paragraph as _; - - let Some(Allocation::Paragraph(paragraph)) = allocation - else { - return None; - }; - - ( - paragraph.buffer(), - Rectangle::new(*position, paragraph.min_bounds()), - paragraph.horizontal_alignment(), - paragraph.vertical_alignment(), - *color, - *clip_bounds, - *transformation, - ) - } - Text::Editor { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Editor as _; - - let Some(Allocation::Editor(editor)) = allocation - else { - return None; - }; - - ( - editor.buffer(), - Rectangle::new(*position, editor.bounds()), - alignment::Horizontal::Left, - alignment::Vertical::Top, - *color, - *clip_bounds, - *transformation, - ) - } - Text::Cached(text) => { - let Some(Allocation::Cache(key)) = allocation else { - return None; - }; - - let entry = cache.get(key).expect("Get cached buffer"); - - ( - &entry.buffer, - Rectangle::new( - text.bounds.position(), - entry.min_bounds, - ), - text.horizontal_alignment, - text.vertical_alignment, - text.color, - text.clip_bounds, - Transformation::IDENTITY, - ) - } - Text::Raw { - raw, - transformation, - } => { - let Some(Allocation::Raw(buffer)) = allocation else { - return None; - }; - - let (width, height) = buffer.size(); - - ( - buffer.as_ref(), - Rectangle::new( - raw.position, - Size::new(width, height), - ), - alignment::Horizontal::Left, - alignment::Vertical::Top, - raw.color, - raw.clip_bounds, - *transformation, - ) - } - }; - - let bounds = bounds * transformation * scale_factor; - - let left = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - let top = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - let clip_bounds = layer_bounds.intersection( - &(clip_bounds * transformation * scale_factor), - )?; - - Some(glyphon::TextArea { - buffer, - left, - top, - scale: scale_factor * transformation.scale_factor(), - bounds: glyphon::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, - }, - default_color: to_color(color), - }) - }, - ); - - let result = renderer.prepare( - device, - queue, - font_system, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - &mut glyphon::SwashCache::new(), - ); - - match result { - Ok(()) => { - self.prepare_layer += 1; - } - Err(glyphon::PrepareError::AtlasFull) => { - // If the atlas cannot grow, then all bets are off. - // Instead of panicking, we will just pray that the result - // will be somewhat readable... + cache, + } => { + storage.prepare( + device, + queue, + &viewport.0, + encoder, + self.format, + &self.state, + cache, + layer_transformation * *transformation, + layer_bounds * layer_transformation, + ); + } } } } pub fn render<'a>( &'a self, - layer: usize, + viewport: &'a Viewport, + storage: &'a Storage, + start: usize, + batch: &'a Batch, bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, - ) { - let renderer = &self.renderers[layer]; + ) -> usize { + let mut layer_count = 0; render_pass.set_scissor_rect( bounds.x, @@ -299,15 +392,252 @@ impl Pipeline { bounds.height, ); - renderer - .render(&self.atlas, render_pass) - .expect("Render text"); + for item in batch { + match item { + Item::Group { .. } => { + let renderer = &self.renderers[start + layer_count]; + + renderer + .render(&self.atlas, &viewport.0, render_pass) + .expect("Render text"); + + layer_count += 1; + } + Item::Cached { cache, .. } => { + if let Some((atlas, upload)) = storage.get(cache) { + upload + .renderer + .render(atlas, &viewport.0, render_pass) + .expect("Render cached text"); + } + } + } + } + + layer_count + } + + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(glyphon::Viewport::new(device, &self.state)) } pub fn end_frame(&mut self) { self.atlas.trim(); - self.cache.get_mut().trim(); + self.cache.trim(); self.prepare_layer = 0; } } + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &glyphon::Viewport, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut glyphon::TextRenderer, + atlas: &mut glyphon::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &[Text], + layer_bounds: Rectangle, + layer_transformation: Transformation, +) -> Result<(), glyphon::PrepareError> { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + enum Allocation { + Paragraph(Paragraph), + Editor(Editor), + Cache(text_cache::KeyHash), + Raw(Arc<glyphon::Buffer>), + } + + let allocations: Vec<_> = sections + .iter() + .map(|section| match section { + Text::Paragraph { paragraph, .. } => { + paragraph.upgrade().map(Allocation::Paragraph) + } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } + Text::Cached { + content, + bounds, + size, + line_height, + font, + shaping, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + text_cache::Key { + content, + size: f32::from(*size), + line_height: f32::from(*line_height), + font: *font, + bounds: Size { + width: bounds.width, + height: bounds.height, + }, + shaping: *shaping, + }, + ); + + Some(Allocation::Cache(key)) + } + Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw), + }) + .collect(); + + let text_areas = sections.iter().zip(allocations.iter()).filter_map( + |(section, allocation)| { + let ( + buffer, + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + transformation, + ) = match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Paragraph as _; + + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + Rectangle::new(*position, paragraph.min_bounds()), + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new(bounds.position(), entry.min_bounds), + *horizontal_alignment, + *vertical_alignment, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new(raw.position, Size::new(width, height)), + alignment::Horizontal::Left, + alignment::Vertical::Top, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let bounds = bounds * transformation * layer_transformation; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let clip_bounds = layer_bounds.intersection( + &(clip_bounds * transformation * layer_transformation), + )?; + + Some(glyphon::TextArea { + buffer, + left, + top, + scale: transformation.scale_factor() + * layer_transformation.scale_factor(), + bounds: glyphon::TextBounds { + left: clip_bounds.x as i32, + top: clip_bounds.y as i32, + right: (clip_bounds.x + clip_bounds.width) as i32, + bottom: (clip_bounds.y + clip_bounds.height) as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + viewport, + text_areas, + &mut glyphon::SwashCache::new(), + ) +} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 2bb6f307..b0551f55 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,14 +1,158 @@ //! Draw meshes of triangles. mod msaa; -use crate::core::{Size, Transformation}; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; -use crate::layer::mesh::{self, Mesh}; use crate::Buffer; +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::rc::{self, Rc}; +use std::sync::atomic::{self, AtomicU64}; + const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + meshes: Vec<Mesh>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + batch: Rc<[Mesh]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(meshes: Vec<Mesh>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if meshes.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + batch: Rc::from(meshes), + version: 0, + }) + } + + pub fn update(&mut self, meshes: Vec<Mesh>) { + self.batch = Rc::from(meshes); + self.version += 1; + } +} + +#[derive(Debug)] +struct Upload { + layer: Layer, + transformation: Transformation, + version: usize, + batch: rc::Weak<[Mesh]>, +} + +#[derive(Debug, Default)] +pub struct Storage { + uploads: FxHashMap<Id, Upload>, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.batch.is_empty() { + return None; + } + + self.uploads.get(&cache.id) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + cache: &Cache, + new_transformation: Transformation, + ) { + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if !cache.batch.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) + { + upload.layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + upload.batch = Rc::downgrade(&cache.batch); + upload.version = cache.version; + upload.transformation = new_transformation; + } + } + hash_map::Entry::Vacant(entry) => { + let mut layer = Layer::new(device, solid, gradient); + + layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + let _ = entry.insert(Upload { + layer, + transformation: new_transformation, + version: 0, + batch: Rc::downgrade(&cache.batch), + }); + + log::debug!( + "New mesh upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + } + + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.batch.strong_count() > 0); + } +} + #[derive(Debug)] pub struct Pipeline { blit: Option<msaa::Blit>, @@ -18,8 +162,198 @@ pub struct Pipeline { prepare_layer: usize, } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, + ) -> Pipeline { + Pipeline { + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + layers: Vec::new(), + prepare_layer: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + storage: &mut Storage, + items: &[Item], + scale: Transformation, + target_size: Size<u32>, + ) { + let projection = if let Some(blit) = &mut self.blit { + blit.prepare(device, encoder, belt, target_size) * scale + } else { + Transformation::orthographic(target_size.width, target_size.height) + * scale + }; + + for item in items { + match item { + Item::Group { + transformation, + meshes, + } => { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.solid, + &self.gradient, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + meshes, + projection * *transformation, + ); + + self.prepare_layer += 1; + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + cache, + projection * *transformation, + ); + } + } + } + } + + pub fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + storage: &Storage, + start: usize, + batch: &Batch, + bounds: Rectangle, + screen_transformation: Transformation, + ) -> usize { + let mut layer_count = 0; + + let items = batch.iter().filter_map(|item| match item { + Item::Group { + transformation, + meshes, + } => { + let layer = &self.layers[start + layer_count]; + layer_count += 1; + + Some(( + layer, + meshes.as_slice(), + screen_transformation * *transformation, + )) + } + Item::Cached { + transformation, + cache, + } => { + let upload = storage.get(cache)?; + + Some(( + &upload.layer, + &cache.batch, + screen_transformation * *transformation, + )) + } + }); + + render( + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + bounds, + items, + ); + + layer_count + } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + } +} + +fn render<'a>( + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut blit: Option<&mut msaa::Blit>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + bounds: Rectangle, + group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>, +) { + { + let (attachment, resolve_target, load) = if let Some(blit) = &mut blit { + let (attachment, resolve_target) = blit.targets(); + + ( + attachment, + Some(resolve_target), + wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + ) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + for (layer, meshes, transformation) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + transformation, + &mut render_pass, + ); + } + } + + if let Some(blit) = blit { + blit.draw(encoder, target); + } +} + #[derive(Debug)] -struct Layer { +pub struct Layer { index_buffer: Buffer<u32>, index_strides: Vec<u32>, solid: solid::Layer, @@ -48,10 +382,11 @@ impl Layer { fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - meshes: &[Mesh<'_>], + meshes: &[Mesh], transformation: Transformation, ) { // Count the total amount of vertices & indices we need to handle @@ -103,33 +438,47 @@ impl Layer { let uniforms = Uniforms::new(transformation * mesh.transformation()); - index_offset += - self.index_buffer.write(queue, index_offset, indices); + index_offset += self.index_buffer.write( + device, + encoder, + belt, + index_offset, + indices, + ); + self.index_strides.push(indices.len() as u32); match mesh { Mesh::Solid { buffers, .. } => { solid_vertex_offset += self.solid.vertices.write( - queue, + device, + encoder, + belt, solid_vertex_offset, &buffers.vertices, ); solid_uniform_offset += self.solid.uniforms.write( - queue, + device, + encoder, + belt, solid_uniform_offset, &[uniforms], ); } Mesh::Gradient { buffers, .. } => { gradient_vertex_offset += self.gradient.vertices.write( - queue, + device, + encoder, + belt, gradient_vertex_offset, &buffers.vertices, ); gradient_uniform_offset += self.gradient.uniforms.write( - queue, + device, + encoder, + belt, gradient_uniform_offset, &[uniforms], ); @@ -142,8 +491,9 @@ impl Layer { &'a self, solid: &'a solid::Pipeline, gradient: &'a gradient::Pipeline, - meshes: &[Mesh<'_>], - scale_factor: f32, + meshes: &[Mesh], + bounds: Rectangle, + transformation: Transformation, render_pass: &mut wgpu::RenderPass<'a>, ) { let mut num_solids = 0; @@ -151,11 +501,12 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); - - if clip_bounds.width < 1 || clip_bounds.height < 1 { + let Some(clip_bounds) = bounds + .intersection(&(mesh.clip_bounds() * transformation)) + .and_then(Rectangle::snap) + else { continue; - } + }; render_pass.set_scissor_rect( clip_bounds.x, @@ -219,117 +570,6 @@ impl Layer { } } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - antialiasing: Option<Antialiasing>, - ) -> Pipeline { - Pipeline { - blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), - solid: solid::Pipeline::new(device, format, antialiasing), - gradient: gradient::Pipeline::new(device, format, antialiasing), - layers: Vec::new(), - prepare_layer: 0, - } - } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - meshes: &[Mesh<'_>], - transformation: Transformation, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered(); - - if self.layers.len() <= self.prepare_layer { - self.layers - .push(Layer::new(device, &self.solid, &self.gradient)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare( - device, - queue, - &self.solid, - &self.gradient, - meshes, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - layer: usize, - target_size: Size<u32>, - meshes: &[Mesh<'_>], - scale_factor: f32, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered(); - - { - let (attachment, resolve_target, load) = if let Some(blit) = - &mut self.blit - { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) - } else { - (target, None, wgpu::LoadOp::Load) - }; - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu.triangle.render_pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { - load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - let layer = &mut self.layers[layer]; - - layer.render( - &self.solid, - &self.gradient, - meshes, - scale_factor, - &mut render_pass, - ); - } - - if let Some(blit) = &mut self.blit { - blit.draw(encoder, target); - } - } - - pub fn end_frame(&mut self) { - self.prepare_layer = 0; - } -} - fn fragment_target( texture_format: wgpu::TextureFormat, ) -> wgpu::ColorTargetState { diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 14abd20b..71c16925 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -1,13 +1,18 @@ +use crate::core::{Size, Transformation}; use crate::graphics; +use std::num::NonZeroU64; + #[derive(Debug)] pub struct Blit { format: wgpu::TextureFormat, pipeline: wgpu::RenderPipeline, constants: wgpu::BindGroup, + ratio: wgpu::Buffer, texture_layout: wgpu::BindGroupLayout, sample_count: u32, targets: Option<Targets>, + last_region: Option<Size<u32>>, } impl Blit { @@ -19,27 +24,52 @@ impl Blit { let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let ratio = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + size: std::mem::size_of::<Ratio>() as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + mapped_at_creation: false, + }); + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::triangle:msaa uniforms layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::triangle::msaa uniforms bind group"), layout: &constant_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], }); let texture_layout = @@ -112,43 +142,61 @@ impl Blit { format, pipeline, constants: constant_bind_group, + ratio, texture_layout, sample_count: antialiasing.sample_count(), targets: None, + last_region: None, } } - pub fn targets( + pub fn prepare( &mut self, device: &wgpu::Device, - width: u32, - height: u32, - ) -> (&wgpu::TextureView, &wgpu::TextureView) { + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + region_size: Size<u32>, + ) -> Transformation { match &mut self.targets { - None => { + Some(targets) + if region_size.width <= targets.size.width + && region_size.height <= targets.size.height => {} + _ => { self.targets = Some(Targets::new( device, self.format, &self.texture_layout, self.sample_count, - width, - height, + region_size, )); } - Some(targets) => { - if targets.width != width || targets.height != height { - self.targets = Some(Targets::new( - device, - self.format, - &self.texture_layout, - self.sample_count, - width, - height, - )); - } - } } + let targets = self.targets.as_mut().unwrap(); + + if Some(region_size) != self.last_region { + let ratio = Ratio { + u: region_size.width as f32 / targets.size.width as f32, + v: region_size.height as f32 / targets.size.height as f32, + }; + + belt.write_buffer( + encoder, + &self.ratio, + 0, + NonZeroU64::new(std::mem::size_of::<Ratio>() as u64) + .expect("non-empty ratio"), + device, + ) + .copy_from_slice(bytemuck::bytes_of(&ratio)); + + self.last_region = Some(region_size); + } + + Transformation::orthographic(targets.size.width, targets.size.height) + } + + pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) { let targets = self.targets.as_ref().unwrap(); (&targets.attachment, &targets.resolve) @@ -191,8 +239,7 @@ struct Targets { attachment: wgpu::TextureView, resolve: wgpu::TextureView, bind_group: wgpu::BindGroup, - width: u32, - height: u32, + size: Size<u32>, } impl Targets { @@ -201,12 +248,11 @@ impl Targets { format: wgpu::TextureFormat, texture_layout: &wgpu::BindGroupLayout, sample_count: u32, - width: u32, - height: u32, + size: Size<u32>, ) -> Targets { let extent = wgpu::Extent3d { - width, - height, + width: size.width, + height: size.height, depth_or_array_layers: 1, }; @@ -252,8 +298,14 @@ impl Targets { attachment, resolve, bind_group, - width, - height, + size, } } } + +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Ratio { + u: f32, + v: f32, +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index fdec1152..b49df899 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,21 +1,49 @@ //! Connect a window with a renderer. use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::color; use crate::graphics::compositor; -use crate::graphics::{Error, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; +use crate::graphics::error; +use crate::graphics::{self, Viewport}; +use crate::settings::{self, Settings}; +use crate::{Engine, Renderer}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { - settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, + engine: Engine, + settings: Settings, +} + +/// A compositor error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// The surface creation failed. + #[error("the surface creation failed: {0}")] + SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError), + /// The surface is not compatible. + #[error("the surface is not compatible")] + IncompatibleSurface, + /// No adapter was found for the options requested. + #[error("no adapter was found for the options requested: {0:?}")] + NoAdapterFound(String), + /// No device request succeeded. + #[error("no device request succeeded: {0:?}")] + RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>), +} + +impl From<Error> for graphics::Error { + fn from(error: Error) -> Self { + Self::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::RequestFailed(error.to_string()), + } + } } impl Compositor { @@ -25,9 +53,9 @@ impl Compositor { pub async fn request<W: compositor::Window>( settings: Settings, compatible_window: Option<W>, - ) -> Option<Self> { + ) -> Result<Self, Error> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: settings.internal_backend, + backends: settings.backends, ..Default::default() }); @@ -36,7 +64,7 @@ impl Compositor { #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) + .enumerate_adapters(settings.backends) .iter() .map(wgpu::Adapter::get_info) .collect(); @@ -47,23 +75,27 @@ impl Compositor { let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); + let adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }; + let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await?; + .request_adapter(&adapter_options) + .await + .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; log::info!("Selected: {:#?}", adapter.get_info()); - let (format, alpha_mode) = - compatible_surface.as_ref().and_then(|surface| { + let (format, alpha_mode) = compatible_surface + .as_ref() + .and_then(|surface| { let capabilities = surface.get_capabilities(&adapter); let mut formats = capabilities.formats.iter().copied(); @@ -90,12 +122,17 @@ impl Compositor { .contains(&wgpu::CompositeAlphaMode::PostMultiplied) { wgpu::CompositeAlphaMode::PostMultiplied + } else if alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied } else { wgpu::CompositeAlphaMode::Auto }; format.zip(Some(preferred_alpha)) - })?; + }) + .ok_or(Error::IncompatibleSurface)?; log::info!( "Selected format: {format:?} with alpha mode: {alpha_mode:?}" @@ -109,74 +146,71 @@ impl Compositor { let limits = [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; - let mut limits = limits.into_iter().map(|limits| wgpu::Limits { + let limits = limits.into_iter().map(|limits| wgpu::Limits { max_bind_groups: 2, ..limits }); - let (device, queue) = - loop { - let required_limits = limits.next()?; - let device = adapter.request_device( + let mut errors = Vec::new(); + + for required_limits in limits { + let result = adapter + .request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), required_features: wgpu::Features::empty(), - required_limits, + required_limits: required_limits.clone(), }, None, - ).await.ok(); + ) + .await; - if let Some(device) = device { - break Some(device); + match result { + Ok((device, queue)) => { + let engine = Engine::new( + &adapter, + &device, + &queue, + format, + settings.antialiasing, + ); + + return Ok(Compositor { + instance, + adapter, + device, + queue, + format, + alpha_mode, + engine, + settings, + }); } - }?; + Err(error) => { + errors.push((required_limits, error)); + } + } + } - Some(Compositor { - instance, - settings, - adapter, - device, - queue, - format, - alpha_mode, - }) - } - - /// Creates a new rendering [`Backend`] for this [`Compositor`]. - pub fn create_backend(&self) -> Backend { - Backend::new( - &self.adapter, - &self.device, - &self.queue, - self.settings, - self.format, - ) + Err(Error::RequestDeviceFailed(errors)) } } -/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and -/// window. -pub fn new<W: compositor::Window>( +/// Creates a [`Compositor`] with the given [`Settings`] and 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 } -/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +/// Presents the given primitives with the given [`Compositor`]. pub fn present( compositor: &mut Compositor, - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, ) -> Result<(), compositor::SurfaceError> { @@ -192,19 +226,20 @@ pub fn present( .texture .create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), frame.texture.format(), view, - primitives, viewport, ); - // Submit work - let _submission = compositor.queue.submit(Some(encoder.finish())); + let _ = compositor.engine.submit(&compositor.queue, encoder); + + // Present the frame frame.present(); Ok(()) @@ -225,20 +260,41 @@ pub fn present( } impl graphics::Compositor for Compositor { - type Settings = Settings; type Renderer = Renderer; type Surface = wgpu::Surface<'static>; - fn new<W: compositor::Window>( - settings: Self::Settings, + async fn with_backend<W: compositor::Window>( + settings: graphics::Settings, compatible_window: W, - ) -> Result<Self, Error> { - new(settings, compatible_window) + backend: Option<&str>, + ) -> Result<Self, graphics::Error> { + match backend { + None | Some("wgpu") => { + let mut settings = Settings::from(settings); + + if let Some(backends) = wgpu::util::backend_bits_from_env() { + settings.backends = backends; + } + + if let Some(present_mode) = settings::present_mode_from_env() { + settings.present_mode = present_mode; + } + + Ok(new(settings, compatible_window).await?) + } + Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } } fn create_renderer(&self) -> Self::Renderer { Renderer::new( - self.create_backend(), + &self.device, + &self.engine, self.settings.default_font, self.settings.default_text_size, ) @@ -278,7 +334,7 @@ impl graphics::Compositor for Compositor { height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 2, + desired_maximum_frame_latency: 1, }, ); } @@ -299,16 +355,7 @@ impl graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present( - self, - backend, - surface, - primitives, - viewport, - background_color, - ) - }) + present(self, renderer, surface, viewport, background_color) } fn screenshot( @@ -318,9 +365,7 @@ impl graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, ) -> Vec<u8> { - renderer.with_primitives(|backend, primitives| { - screenshot(self, backend, primitives, viewport, background_color) - }) + screenshot(self, renderer, viewport, background_color) } } @@ -328,18 +373,11 @@ impl graphics::Compositor for Compositor { /// /// Returns RGBA bytes of the texture data. pub fn screenshot( - compositor: &Compositor, - backend: &mut Backend, - primitives: &[Primitive], + compositor: &mut Compositor, + renderer: &mut Renderer, viewport: &Viewport, background_color: Color, ) -> Vec<u8> { - let mut encoder = compositor.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu.offscreen.encoder"), - }, - ); - let dimensions = BufferDimensions::new(viewport.physical_size()); let texture_extent = wgpu::Extent3d { @@ -363,14 +401,20 @@ pub fn screenshot( let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), texture.format(), &view, - primitives, viewport, ); @@ -407,7 +451,7 @@ pub fn screenshot( texture_extent, ); - let index = compositor.queue.submit(Some(encoder.finish())); + let index = compositor.engine.submit(&compositor.queue, encoder); let slice = output_buffer.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c4..3c9f6a54 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -21,13 +24,14 @@ svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] wgpu = ["iced_renderer/wgpu"] +advanced = [] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true -iced_style.workspace = true num-traits.workspace = true +rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 867fbfaf..dc949671 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,8 +48,8 @@ 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, + Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, on_press: Option<Message>, @@ -62,13 +57,13 @@ where height: Length, padding: Padding, clip: bool, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, + Theme: Catalog, { /// Creates a new [`Button`] with the given content. pub fn new( @@ -82,9 +77,9 @@ where on_press: None, width: size.width.fluid(), height: size.height.fluid(), - padding: Padding::new(5.0), + padding: DEFAULT_PADDING, clip: false, - style: Theme::Style::default(), + class: Theme::default(), } } @@ -123,33 +118,50 @@ where self } - /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); - 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 } + + /// Sets the style of the [`Button`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Button`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + 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, + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn children(&self) -> Vec<Tree> { @@ -173,13 +185,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( @@ -223,9 +241,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( @@ -240,16 +297,39 @@ where ) { 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 style = theme.style(&self.class, status); + + if style.background.is_some() + || style.border.width > 0.0 + || style.shadow.color.a > 0.0 + { + renderer.fill_quad( + renderer::Quad { + bounds, + border: style.border, + shadow: style.shadow, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } let viewport = if self.clip { bounds.intersection(viewport).unwrap_or(*viewport) @@ -262,7 +342,7 @@ where renderer, theme, &renderer::Style { - text_color: styling.text_color, + text_color: style.text_color, }, content_layout, cursor, @@ -278,7 +358,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>( @@ -301,7 +387,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: Catalog + 'a, Renderer: crate::core::Renderer + 'a, { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { @@ -309,143 +395,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 style of a button. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// 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 Style { + /// Updates the [`Style`] 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 Default for Style { + 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; - - let bounds = layout.bounds(); - - if cursor.is_over(bounds) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); - - state.is_pressed = false; - } - _ => {} } - - event::Status::Ignored } -/// Draws a [`Button`]. -pub fn draw<'a, 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); +/// The theme catalog of a [`Button`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; - let styling = if !is_enabled { - theme.disabled(style) - } else if is_mouse_over { - let state = state(); + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; - if state.is_pressed { - theme.pressed(style) - } else { - theme.hovered(style) - } - } else { - theme.active(style) + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Button`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// A primary button; denoting a main action. +pub fn primary(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + let base = styled(palette.primary.strong); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + background: Some(Background::Color(palette.primary.base.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} + +/// A secondary button; denoting a complementary action. +pub fn secondary(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + let base = styled(palette.secondary.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + background: Some(Background::Color(palette.secondary.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} + +/// A success button; denoting a good outcome. +pub fn success(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + let base = styled(palette.success.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + background: Some(Background::Color(palette.success.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} + +/// A danger button; denoting a destructive action. +pub fn danger(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + let base = styled(palette.danger.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + 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) -> Style { + let palette = theme.extended_palette(); + + let base = Style { + text_color: palette.background.base.text, + ..Style::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)), - ); - } - - 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) -} - -/// 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() + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + text_color: palette.background.base.text.scale_alpha(0.8), + ..base + }, + Status::Disabled => disabled(base), + } +} + +fn styled(pair: palette::Pair) -> Style { + Style { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border::rounded(2), + ..Style::default() + } +} + +fn disabled(style: Style) -> Style { + Style { + background: style + .background + .map(|background| background.scale_alpha(0.5)), + text_color: style.text_color.scale_alpha(0.5), + ..style } } diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 0eda0191..be09f163 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -6,8 +6,11 @@ mod program; pub use event::Event; pub use program::Program; -pub use crate::graphics::geometry::*; -pub use crate::renderer::geometry::*; +pub use crate::graphics::cache::Group; +pub use crate::graphics::geometry::{ + fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, + Path, Stroke, Style, Text, +}; use crate::core; use crate::core::layout::{self, Layout}; @@ -15,12 +18,25 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget, + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; use crate::graphics::geometry; use std::marker::PhantomData; +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +pub type Cache<Renderer = crate::Renderer> = geometry::Cache<Renderer>; + +/// The geometry supported by a renderer. +pub type Geometry<Renderer = crate::Renderer> = + <Renderer as geometry::Renderer>::Geometry; + +/// The frame supported by a renderer. +pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>; + /// A widget capable of drawing 2D graphics. /// /// ## Drawing a simple circle @@ -42,7 +58,7 @@ use std::marker::PhantomData; /// impl Program<()> for Circle { /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry>{ +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry> { /// // We prepare a new `Frame` /// let mut frame = Frame::new(renderer, bounds.size()); /// @@ -207,12 +223,15 @@ where let state = tree.state.downcast_ref::<P::State>(); - renderer.with_transformation( - Transformation::translate(bounds.x, bounds.y), + renderer.with_translation( + Vector::new(bounds.x, bounds.y), |renderer| { - renderer.draw( - self.program.draw(state, renderer, theme, bounds, cursor), - ); + let layers = + self.program.draw(state, renderer, theme, bounds, cursor); + + for layer in layers { + renderer.draw_geometry(layer); + } }, ); } diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index 0bff4bda..a7ded0f4 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,5 +1,6 @@ use crate::canvas::event::{self, Event}; use crate::canvas::mouse; +use crate::canvas::Geometry; use crate::core::Rectangle; use crate::graphics::geometry; @@ -52,7 +53,7 @@ where theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Renderer::Geometry>; + ) -> Vec<Geometry<Renderer>>; /// Returns the current mouse interaction of the [`Program`]. /// @@ -94,7 +95,7 @@ where theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, - ) -> Vec<Renderer::Geometry> { + ) -> Vec<Geometry<Renderer>> { T::draw(self, state, renderer, theme, bounds, cursor) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 0ff4d58b..225c316d 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,8 +38,8 @@ pub struct Checkbox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, + Theme: Catalog, { is_checked: bool, on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>, @@ -53,19 +52,19 @@ pub struct Checkbox< text_shaping: text::Shaping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, - style: <Theme as StyleSheet>::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> where Renderer: text::Renderer, - Theme: StyleSheet + crate::text::StyleSheet, + Theme: Catalog, { /// 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`]. /// @@ -91,7 +90,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, - style: Default::default(), + class: Theme::default(), } } @@ -174,11 +173,20 @@ where } /// Sets the style of the [`Checkbox`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Checkbox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -186,8 +194,8 @@ 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, + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<widget::text::State<Renderer::Paragraph>>() @@ -286,24 +294,27 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { 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 style = theme.style(&self.class, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -311,10 +322,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: custom_style.border, + border: style.border, ..renderer::Quad::default() }, - custom_style.background, + style.background, ); let Icon { @@ -329,7 +340,7 @@ where if self.is_checked { renderer.fill_text( text::Text { - content: &code_point.to_string(), + content: code_point.to_string(), font: *font, size, line_height: *line_height, @@ -339,7 +350,7 @@ where shaping: *shaping, }, bounds.center(), - custom_style.icon_color, + style.icon_color, *viewport, ); } @@ -350,11 +361,11 @@ where crate::text::draw( renderer, - style, + defaults, label_layout, tree.state.downcast_ref(), - crate::text::Appearance { - color: custom_style.text_color, + crate::text::Style { + color: style.text_color, }, viewport, ); @@ -366,7 +377,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 + Catalog, Renderer: 'a + text::Renderer, { fn from( @@ -390,3 +401,191 @@ 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 style of a checkbox. +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// 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 theme catalog of a [`Checkbox`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Checkbox`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// A primary checkbox; denoting a main toggle. +pub fn primary(theme: &Theme, status: Status) -> Style { + 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) -> Style { + 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) -> Style { + 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) -> Style { + 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, +) -> Style { + Style { + 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 d37ef695..df7829b3 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -33,11 +33,18 @@ where Self::from_vec(Vec::new()) } + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + /// 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) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Creates a [`Column`] from an already allocated [`Vec`]. diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index e3862174..253850df 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,7 @@ pub struct ComboBox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: text_input::StyleSheet + menu::StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { state: &'a State<T>, @@ -43,7 +43,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_class: <Theme as menu::Catalog>::Class<'a>, padding: Padding, size: Option<f32>, } @@ -51,7 +51,7 @@ 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, + Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, @@ -64,7 +64,8 @@ where on_selected: impl Fn(T) -> Message + 'static, ) -> Self { let text_input = TextInput::new(placeholder, &state.value()) - .on_input(TextInputEvent::TextChanged); + .on_input(TextInputEvent::TextChanged) + .class(Theme::default_input()); let selection = selection.map(T::to_string).unwrap_or_default(); @@ -77,7 +78,7 @@ where on_option_hovered: None, on_input: None, on_close: None, - menu_style: Default::default(), + menu_class: <Theme as Catalog>::default_menu(), padding: text_input::DEFAULT_PADDING, size: None, } @@ -117,28 +118,6 @@ where self } - /// Sets the style of the [`ComboBox`]. - // TODO: Define its own `StyleSheet` trait - pub fn style<S>(mut self, style: S) -> Self - where - S: Into<<Theme as text_input::StyleSheet>::Style> - + Into<<Theme as menu::StyleSheet>::Style> - + Clone, - { - self.menu_style = style.clone().into(); - self.text_input = self.text_input.style(style); - self - } - - /// 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 - } - /// Sets the [`Renderer::Font`] of the [`ComboBox`]. /// /// [`Renderer::Font`]: text::Renderer @@ -176,6 +155,55 @@ where ..self } } + + /// Sets the style of the input of the [`ComboBox`]. + #[must_use] + pub fn input_style( + mut self, + style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a, + ) -> Self + where + <Theme as text_input::Catalog>::Class<'a>: + From<text_input::StyleFn<'a, Theme>>, + { + self.text_input = self.text_input.style(style); + self + } + + /// Sets the style of the menu of the [`ComboBox`]. + #[must_use] + pub fn menu_style( + mut self, + style: impl Fn(&Theme) -> menu::Style + 'a, + ) -> Self + where + <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>, + { + self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the input of the [`ComboBox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn input_class( + mut self, + class: impl Into<<Theme as text_input::Catalog>::Class<'a>>, + ) -> Self { + self.text_input = self.text_input.class(class); + self + } + + /// Sets the style class of the menu of the [`ComboBox`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn menu_class( + mut self, + class: impl Into<<Theme as menu::Catalog>::Class<'a>>, + ) -> Self { + self.menu_class = class.into(); + self + } } /// The local state of a [`ComboBox`]. @@ -299,10 +327,7 @@ 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, + Theme: Catalog, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -675,38 +700,47 @@ where .. } = tree.state.downcast_mut::<Menu<T>>(); - let bounds = layout.bounds(); - self.state.sync_filtered_options(filtered_options); - let mut menu = menu::Menu::new( - menu, - &filtered_options.options, - hovered_option, - |x| { - tree.children[0] - .state - .downcast_mut::<text_input::State<Renderer::Paragraph>>( - ) - .unfocus(); + if filtered_options.options.is_empty() { + None + } else { + let bounds = layout.bounds(); - (self.on_selected)(x) - }, - self.on_option_hovered.as_deref(), - ) - .width(bounds.width) - .padding(self.padding) - .style(self.menu_style.clone()); + let mut menu = menu::Menu::new( + menu, + &filtered_options.options, + hovered_option, + |x| { + tree.children[0] + .state + .downcast_mut::<text_input::State<Renderer::Paragraph>>( + ) + .unfocus(); - if let Some(font) = self.font { - menu = menu.font(font); + (self.on_selected)(x) + }, + self.on_option_hovered.as_deref(), + &self.menu_class, + ) + .width(bounds.width) + .padding(self.padding); + + if let Some(font) = self.font { + menu = menu.font(font); + } + + if let Some(size) = self.size { + menu = menu.text_size(size); + } + + Some( + menu.overlay( + layout.position() + translation, + bounds.height, + ), + ) } - - if let Some(size) = self.size { - menu = menu.text_size(size); - } - - Some(menu.overlay(layout.position() + translation, bounds.height)) } else { None } @@ -719,11 +753,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: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { @@ -731,8 +761,22 @@ where } } -/// Search list of options for a given query. -pub fn search<'a, T, A>( +/// The theme catalog of a [`ComboBox`]. +pub trait Catalog: text_input::Catalog + menu::Catalog { + /// The default class for the text input of the [`ComboBox`]. + fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> { + <Self as text_input::Catalog>::default() + } + + /// The default class for the menu of the [`ComboBox`]. + fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> { + <Self as menu::Catalog>::default() + } +} + +impl Catalog for Theme {} + +fn search<'a, T, A>( options: impl IntoIterator<Item = T> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a, query: &'a str, @@ -759,8 +803,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 diff --git a/widget/src/container.rs b/widget/src/container.rs index e0174177..8b6638d4 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,12 @@ 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, + self, 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,8 +25,8 @@ pub struct Container< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { id: Option<Id>, padding: Padding, @@ -36,21 +36,20 @@ 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>, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { - /// Creates an empty [`Container`]. - pub fn new<T>(content: T) -> Self - where - T: Into<Element<'a, Message, Theme, Renderer>>, - { + /// Creates a [`Container`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { let content = content.into(); let size = content.as_widget().size_hint(); @@ -63,8 +62,8 @@ where max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - style: Default::default(), clip: false, + class: Theme::default(), content, } } @@ -93,6 +92,49 @@ where self } + /// Sets the [`Container`] to fill the available space in the horizontal axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_x`]. + /// + /// Calling this method is equivalent to calling [`width`] with a + /// [`Length::Fill`]. + /// + /// [`center_x`]: Self::center_x + /// [`width`]: Self::width + pub fn fill_x(self) -> Self { + self.width(Length::Fill) + } + + /// Sets the [`Container`] to fill the available space in the vetical axis. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center_y`]. + /// + /// Calling this method is equivalent to calling [`height`] with a + /// [`Length::Fill`]. + /// + /// [`center_y`]: Self::center_x + /// [`height`]: Self::height + pub fn fill_y(self) -> Self { + self.height(Length::Fill) + } + + /// Sets the [`Container`] to fill all the available space. + /// + /// This can be useful to quickly position content when chained with + /// alignment functions—like [`center`]. + /// + /// Calling this method is equivalent to chaining [`fill_x`] and + /// [`fill_y`]. + /// + /// [`center`]: Self::center + /// [`fill_x`]: Self::fill_x + /// [`fill_y`]: Self::fill_y + pub fn fill(self) -> Self { + self.width(Length::Fill).height(Length::Fill) + } + /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -117,22 +159,31 @@ where self } - /// Centers the contents in the horizontal axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the horizontal axis + /// and centers its contents there. pub fn center_x(mut self) -> Self { + self.width = Length::Fill; self.horizontal_alignment = alignment::Horizontal::Center; self } - /// Centers the contents in the vertical axis of the [`Container`]. + /// Sets the [`Container`] to fill the available space in the vertical axis + /// and centers its contents there. pub fn center_y(mut self) -> Self { + self.height = Length::Fill; self.vertical_alignment = alignment::Vertical::Center; self } - /// Sets the style of the [`Container`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); - self + /// Centers the contents in both the horizontal and vertical axes of the + /// [`Container`]. + /// + /// This is equivalent to chaining [`center_x`] and [`center_y`]. + /// + /// [`center_x`]: Self::center_x + /// [`center_y`]: Self::center_y + pub fn center(self) -> Self { + self.center_x().center_y() } /// Sets whether the contents of the [`Container`] should be clipped on @@ -141,13 +192,31 @@ where self.clip = clip; self } + + /// Sets the style of the [`Container`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Container`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { self.content.as_widget().tag() @@ -262,10 +331,11 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let style = theme.appearance(&self.style); + let bounds = layout.bounds(); + let style = theme.style(&self.class); - if let Some(clipped_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, @@ -307,8 +377,8 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + StyleSheet, - Renderer: 'a + crate::core::Renderer, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( column: Container<'a, Message, Theme, Renderer>, @@ -345,25 +415,25 @@ pub fn layout( ) } -/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. +/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. pub fn draw_background<Renderer>( renderer: &mut Renderer, - appearance: &Appearance, + style: &Style, bounds: Rectangle, ) where - Renderer: crate::core::Renderer, + Renderer: core::Renderer, { - if appearance.background.is_some() - || appearance.border.width > 0.0 - || appearance.shadow.color.a > 0.0 + if style.background.is_some() + || style.border.width > 0.0 + || style.shadow.color.a > 0.0 { renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, - shadow: appearance.shadow, + border: style.border, + shadow: style.shadow, }, - appearance + style .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); @@ -482,3 +552,118 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { bounds: None, }) } + +/// The appearance of a container. +#[derive(Debug, Clone, Copy, Default)] +pub struct Style { + /// 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 Style { + /// Updates the border of the [`Style`] 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 [`Style`]. + pub fn with_background(self, background: impl Into<Background>) -> Self { + Self { + background: Some(background.into()), + ..self + } + } +} + +impl From<Color> for Style { + fn from(color: Color) -> Self { + Self::default().with_background(color) + } +} + +impl From<Gradient> for Style { + fn from(gradient: Gradient) -> Self { + Self::default().with_background(gradient) + } +} + +impl From<gradient::Linear> for Style { + fn from(gradient: gradient::Linear) -> Self { + Self::default().with_background(gradient) + } +} + +/// The theme catalog of a [`Container`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`Container`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(transparent) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// A transparent [`Container`]. +pub fn transparent<Theme>(_theme: &Theme) -> Style { + Style::default() +} + +/// A rounded [`Container`] with a background. +pub fn rounded_box(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + background: Some(palette.background.weak.color.into()), + border: Border::rounded(2), + ..Style::default() + } +} + +/// A bordered [`Container`] with a background. +pub fn bordered_box(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 1.0, + radius: 0.0.into(), + color: palette.background.strong.color, + }, + ..Style::default() + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index f5cceff9..77eacaf5 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; -use crate::core::{Element, Length, Pixels}; +use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; use crate::pane_grid::{self, PaneGrid}; @@ -16,13 +16,13 @@ use crate::rule::{self, Rule}; use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; -use crate::style::application; use crate::text::{self, Text}; use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; -use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; +use crate::vertical_slider::{self, VerticalSlider}; +use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -53,6 +53,19 @@ macro_rules! row { ); } +/// Creates a [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +#[macro_export] +macro_rules! stack { + () => ( + $crate::Stack::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Stack::with_children([$($crate::core::Element::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -60,12 +73,33 @@ 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::Catalog + 'a, Renderer: core::Renderer, { Container::new(content) } +/// Creates a new [`Container`] that fills all the available space +/// and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let centered = container("Centered!").center(); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).fill().center() +} + /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, @@ -99,6 +133,428 @@ where Row::with_children(children) } +/// Creates a new [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +pub fn stack<'a, Message, Theme, Renderer>( + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, +) -> Stack<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Stack::with_children(children) +} + +/// Wraps the given widget and captures any mouse button presses inside the bounds of +/// the widget—effectively making it _opaque_. +/// +/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse +/// events from passing through layers. +/// +/// [`Stack`]: crate::Stack +pub fn opaque<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Opaque<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + } + + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Opaque<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec<Tree> { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size<Length> { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation<Message>, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_mouse_press = matches!( + event, + core::Event::Mouse(mouse::Event::ButtonPressed(_)) + ); + + if let core::event::Status::Captured = + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ) + { + return event::Status::Captured; + } + + if is_mouse_press && cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &core::widget::Tree, + layout: core::Layout<'_>, + cursor: core::mouse::Cursor, + viewport: &core::Rectangle, + renderer: &Renderer, + ) -> core::mouse::Interaction { + let interaction = self + .content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer); + + if interaction == mouse::Interaction::None + && cursor.is_over(layout.bounds()) + { + mouse::Interaction::Idle + } else { + interaction + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>> + { + self.content.as_widget_mut().overlay( + state, + layout, + renderer, + translation, + ) + } + } + + Element::new(Opaque { + content: content.into(), + }) +} + +/// Displays a widget on top of another one, only when the base widget is hovered. +/// +/// This works analogously to a [`stack`], but it will only display the layer on top +/// when the cursor is over the base. It can be useful for removing visual clutter. +/// +/// [`stack`]: stack() +pub fn hover<'a, Message, Theme, Renderer>( + base: impl Into<Element<'a, Message, Theme, Renderer>>, + top: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Hover<'a, Message, Theme, Renderer> { + base: Element<'a, Message, Theme, Renderer>, + top: Element<'a, Message, Theme, Renderer>, + is_top_overlay_active: bool, + } + + impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Hover<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + struct Tag; + tree::Tag::of::<Tag>() + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.base), Tree::new(&self.top)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.top]); + } + + fn size(&self) -> Size<Length> { + self.base.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.base.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let base = self.base.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let top = self.top.as_widget().layout( + &mut tree.children[1], + renderer, + &layout::Limits::new(Size::ZERO, base.size()), + ); + + layout::Node::with_children(base.size(), vec![base, top]) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(bounds) = layout.bounds().intersection(viewport) { + let mut children = layout.children().zip(&tree.children); + + let (base_layout, base_tree) = children.next().unwrap(); + + self.base.as_widget().draw( + base_tree, + renderer, + theme, + style, + base_layout, + cursor, + viewport, + ); + + if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + { + let (top_layout, top_tree) = children.next().unwrap(); + + renderer.with_layer(bounds, |renderer| { + self.top.as_widget().draw( + top_tree, renderer, theme, style, top_layout, + cursor, viewport, + ); + }); + } + } + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation<Message>, + ) { + let children = [&self.base, &self.top] + .into_iter() + .zip(layout.children().zip(&mut tree.children)); + + for (child, (layout, tree)) in children { + child.as_widget().operate(tree, layout, renderer, operation); + } + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let mut children = layout.children().zip(&mut tree.children); + let (base_layout, base_tree) = children.next().unwrap(); + + let top_status = if matches!( + event, + Event::Mouse( + mouse::Event::CursorMoved { .. } + | mouse::Event::ButtonReleased(_) + ) + ) || cursor.is_over(layout.bounds()) + { + let (top_layout, top_tree) = children.next().unwrap(); + + self.top.as_widget_mut().on_event( + top_tree, + event.clone(), + top_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } else { + event::Status::Ignored + }; + + if top_status == event::Status::Captured { + return top_status; + } + + self.base.as_widget_mut().on_event( + base_tree, + event.clone(), + base_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + [&self.base, &self.top] + .into_iter() + .rev() + .zip(layout.children().rev().zip(tree.children.iter().rev())) + .map(|(child, (layout, tree))| { + child.as_widget().mouse_interaction( + tree, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<core::overlay::Element<'b, Message, Theme, Renderer>> + { + let mut overlays = [&mut self.base, &mut self.top] + .into_iter() + .zip(layout.children().zip(tree.children.iter_mut())) + .map(|(child, (layout, tree))| { + child.as_widget_mut().overlay( + tree, + layout, + renderer, + translation, + ) + }); + + if let Some(base_overlay) = overlays.next()? { + return Some(base_overlay); + } + + let top_overlay = overlays.next()?; + self.is_top_overlay_active = top_overlay.is_some(); + + top_overlay + } + } + + Element::new(Hover { + base: base.into(), + top: top.into(), + is_top_overlay_active: false, + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable @@ -106,7 +562,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::Catalog + 'a, Renderer: core::Renderer, { Scrollable::new(content) @@ -119,8 +575,8 @@ pub fn button<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Button<'a, Message, Theme, Renderer> where + Theme: button::Catalog + 'a, Renderer: core::Renderer, - Theme: button::StyleSheet, { Button::new(content) } @@ -136,7 +592,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::Catalog + 'a, Renderer: core::text::Renderer, { Tooltip::new(content, tooltip, position) @@ -146,13 +602,26 @@ where /// /// [`Text`]: core::widget::Text pub fn text<'a, Theme, Renderer>( - text: impl ToString, + text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> where - Theme: text::StyleSheet, + Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { - Text::new(text.to_string()) + Text::new(text) +} + +/// Creates a new [`Text`] widget that displays the provided value. +/// +/// [`Text`]: core::widget::Text +pub fn value<'a, Theme, Renderer>( + value: impl ToString, +) -> Text<'a, Theme, Renderer> +where + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, +{ + Text::new(value.to_string()) } /// Creates a new [`Checkbox`]. @@ -163,7 +632,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>( is_checked: bool, ) -> Checkbox<'a, Message, Theme, Renderer> where - Theme: checkbox::StyleSheet + text::StyleSheet, + Theme: checkbox::Catalog + 'a, Renderer: core::text::Renderer, { Checkbox::new(label, is_checked) @@ -172,15 +641,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::Catalog + 'a, Renderer: core::text::Renderer, V: Copy + Eq, { @@ -196,8 +665,8 @@ pub fn toggler<'a, Message, Theme, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where + Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, - Theme: toggler::StyleSheet, { Toggler::new(label, is_checked, f) } @@ -211,7 +680,7 @@ pub fn text_input<'a, Message, Theme, Renderer>( ) -> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: text_input::StyleSheet, + Theme: text_input::Catalog + 'a, Renderer: core::text::Renderer, { TextInput::new(placeholder, value) @@ -220,12 +689,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::Catalog + 'a, Renderer: core::text::Renderer, { TextEditor::new(content) @@ -242,7 +711,7 @@ pub fn slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::StyleSheet, + Theme: slider::Catalog + 'a, { Slider::new(range, value, on_change) } @@ -258,7 +727,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::Catalog + 'a, { VerticalSlider::new(range, value, on_change) } @@ -276,13 +745,8 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, + Theme: pick_list::Catalog + overlay::menu::Catalog, 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) } @@ -298,7 +762,7 @@ 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::Catalog + 'a, Renderer: core::text::Renderer, { ComboBox::new(state, placeholder, selection, on_selected) @@ -323,9 +787,9 @@ pub fn vertical_space() -> Space { /// 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::Catalog + 'a, { Rule::horizontal(height) } @@ -333,9 +797,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::Catalog + 'a, { Rule::vertical(width) } @@ -347,12 +811,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::Catalog + 'a, { ProgressBar::new(range, value) } @@ -370,9 +834,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::Catalog, { crate::Svg::new(handle) } @@ -396,9 +862,11 @@ where /// [`QRCode`]: crate::QRCode /// [`Data`]: crate::qr_code::Data #[cfg(feature = "qr_code")] -pub fn qr_code<Theme>(data: &crate::qr_code::Data) -> crate::QRCode<'_, Theme> +pub fn qr_code<'a, Theme>( + data: &'a crate::qr_code::Data, +) -> crate::QRCode<'a, Theme> where - Theme: crate::qr_code::StyleSheet, + Theme: crate::qr_code::Catalog + 'a, { crate::QRCode::new(data) } @@ -440,16 +908,23 @@ where MouseArea::new(widget) } -/// Creates a new [`Themer`]. -pub fn themer<'a, Message, Theme, Renderer>( - theme: Theme, - content: impl Into<Element<'a, Message, Theme, Renderer>>, -) -> Themer<'a, Message, Theme, Renderer> +/// A widget that applies any `Theme` to its contents. +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, - Theme: application::StyleSheet, + NewTheme: Clone, { - Themer::new(theme, content) + Themer::new(move |_| new_theme.clone(), content) } /// Creates a new [`PaneGrid`]. @@ -463,7 +938,7 @@ pub fn pane_grid<'a, T, Message, Theme, Renderer>( ) -> PaneGrid<'a, Message, Theme, Renderer> where Renderer: core::Renderer, - Theme: pane_grid::StyleSheet + container::StyleSheet, + Theme: pane_grid::Catalog, { PaneGrid::new(state, view) } diff --git a/widget/src/image.rs b/widget/src/image.rs index ccf1f175..80e17263 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,11 +8,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; -use std::hash::Hash; - pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. @@ -38,6 +37,8 @@ pub struct Image<Handle> { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, } impl<Handle> Image<Handle> { @@ -47,8 +48,10 @@ impl<Handle> Image<Handle> { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), + rotation: Rotation::default(), + opacity: 1.0, } } @@ -77,6 +80,21 @@ impl<Handle> Image<Handle> { self.filter_method = filter_method; self } + + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Image`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -87,22 +105,24 @@ pub fn layout<Renderer, Handle>( width: Length, height: Length, content_fit: ContentFit, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer<Handle = Handle>, { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.dimensions(handle); + let image_size = renderer.measure_image(handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - Size::new(width as f32, height as f32) - }; + // The rotated size of the image + let rotated_size = rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(width, height, image_size); + let raw_size = limits.resolve(width, height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); + let full_size = content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -126,29 +146,47 @@ pub fn draw<Renderer, Handle>( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, + opacity: f32, ) where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); + let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); + + let scale = Vector::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + + let final_size = image_size * scale; + + let position = match content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + renderer.draw_image( + handle.clone(), + filter_method, + drawing_bounds, + rotation.radians(), + opacity, ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -163,7 +201,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn size(&self) -> Size<Length> { Size { @@ -185,6 +223,7 @@ where self.width, self.height, self.content_fit, + self.rotation, ) } @@ -204,6 +243,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.opacity, ); } } @@ -212,7 +253,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 9666ff9f..8fe6f021 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,12 +6,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; -use std::hash::Hash; - /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] pub struct Viewer<Handle> { @@ -40,6 +38,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; @@ -88,7 +92,7 @@ impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Viewer<Handle> where Renderer: image::Renderer<Handle = Handle>, - Handle: Clone + Hash, + Handle: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -111,7 +115,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_image(&self.handle); let mut size = limits.resolve( self.width, @@ -126,7 +130,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; @@ -212,7 +216,7 @@ where event::Status::Captured } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let Some(cursor_position) = cursor.position() else { + let Some(cursor_position) = cursor.position_over(bounds) else { return event::Status::Ignored; }; @@ -298,7 +302,7 @@ where } else if is_mouse_over { mouse::Interaction::Grab } else { - mouse::Interaction::Idle + mouse::Interaction::None } } @@ -329,8 +333,7 @@ where renderer.with_layer(bounds, |renderer| { renderer.with_translation(translation, |renderer| { - image::Renderer::draw( - renderer, + renderer.draw_image( self.handle.clone(), self.filter_method, Rectangle { @@ -338,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + Radians(0.0), + 1.0, ); }); }); @@ -396,7 +401,7 @@ impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>> where Renderer: 'a + image::Renderer<Handle = Handle>, Message: 'a, - Handle: Clone + Hash + 'a, + Handle: Clone + 'a, { fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(viewer) @@ -415,7 +420,7 @@ pub fn image_size<Renderer>( where Renderer: image::Renderer, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.measure_image(handle); let (width, height) = { let dimensions = (width as f32, height as f32); diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 8a8d5fe7..fdaadefa 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -40,27 +40,49 @@ where { /// Creates an empty [`Column`]. pub fn new() -> Self { - Column { + Self::from_vecs(Vec::new(), Vec::new()) + } + + /// Creates a [`Column`] from already allocated [`Vec`]s. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, 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_vecs( + keys: Vec<Key>, + 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, - keys: Vec::new(), - children: Vec::new(), + keys, + children, } } + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vecs( + Vec::with_capacity(capacity), + Vec::with_capacity(capacity), + ) + } + /// Creates a [`Column`] with the given elements. pub fn with_children( children: impl IntoIterator< Item = (Key, Element<'a, Message, Theme, Renderer>), >, ) -> Self { - children - .into_iter() - .fold(Self::new(), |column, (key, child)| column.push(key, child)) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Sets the vertical spacing _between_ elements. @@ -132,6 +154,18 @@ where self } } + + /// Extends the [`Column`] with the given children. + pub fn extend( + self, + children: impl IntoIterator< + Item = (Key, Element<'a, Message, Theme, Renderer>), + >, + ) -> Self { + children + .into_iter() + .fold(self, |column, (key, child)| column.push(key, child)) + } } impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> @@ -190,7 +224,7 @@ where ); if state.keys != self.keys { - state.keys = self.keys.clone(); + state.keys.clone_from(&self.keys); } } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index eb663ea5..04783dbe 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -18,11 +18,12 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::Element; use crate::core::{ - self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; use ouroboros::self_referencing; +use rustc_hash::FxHasher; use std::cell::RefCell; use std::hash::{Hash, Hasher as H}; use std::rc::Rc; @@ -106,9 +107,12 @@ where } fn state(&self) -> tree::State { - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let hash = hasher.finish(); + let hash = { + let mut hasher = FxHasher::default(); + self.dependency.hash(&mut hasher); + + hasher.finish() + }; let element = Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); @@ -127,9 +131,12 @@ where .state .downcast_mut::<Internal<Message, Theme, Renderer>>(); - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let new_hash = hasher.finish(); + let new_hash = { + let mut hasher = FxHasher::default(); + self.dependency.hash(&mut hasher); + + hasher.finish() + }; if current.hash != new_hash { current.hash = new_hash; diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index a512e0de..7ba71a02 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -478,12 +478,14 @@ where translation: Vector, ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { self.rebuild_element_if_necessary(); + let tree = tree .state .downcast_mut::<Rc<RefCell<Option<Tree>>>>() .borrow_mut() .take() .unwrap(); + let overlay = Overlay(Some( InnerBuilder { instance: self, diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 313e1edb..f612102e 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -308,10 +308,13 @@ where content_layout_node.as_ref().unwrap(), ); - element - .as_widget_mut() - .overlay(tree, content_layout, renderer, translation) - .map(|overlay| RefCell::new(Nested::new(overlay))) + ( + element + .as_widget_mut() + .overlay(tree, content_layout, renderer, translation) + .map(|overlay| RefCell::new(Nested::new(overlay))), + content_layout_node, + ) }, } .build(); @@ -341,7 +344,10 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { #[borrows(mut content, mut tree)] #[not_covariant] - overlay: Option<RefCell<Nested<'this, Message, Theme, Renderer>>>, + overlay: ( + Option<RefCell<Nested<'this, Message, Theme, Renderer>>>, + &'this mut Option<layout::Node>, + ), } impl<'a, 'b, Message, Theme, Renderer> @@ -351,7 +357,7 @@ impl<'a, 'b, Message, Theme, Renderer> &self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option<T> { - self.with_overlay(|overlay| { + self.with_overlay(|(overlay, _layout)| { overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) }) } @@ -360,7 +366,7 @@ impl<'a, 'b, Message, Theme, Renderer> &mut self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, ) -> Option<T> { - self.with_overlay_mut(|overlay| { + self.with_overlay_mut(|(overlay, _layout)| { overlay.as_mut().map(|nested| (f)(nested.get_mut())) }) } @@ -412,10 +418,27 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell) - }) - .unwrap_or(event::Status::Ignored) + let mut is_layout_invalid = false; + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + let event_status = overlay.on_event( + event, layout, cursor, renderer, clipboard, shell, + ); + + is_layout_invalid = shell.is_layout_invalid(); + + event_status + }) + .unwrap_or(event::Status::Ignored); + + if is_layout_invalid { + self.with_overlay_mut(|(_overlay, layout)| { + **layout = None; + }); + } + + event_status } fn is_over( diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbe..00e9aaa4 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -2,23 +2,17 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] 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 stack; mod themer; pub mod button; @@ -34,7 +28,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; @@ -86,6 +79,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use space::Space; #[doc(no_inline)] +pub use stack::Stack; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_editor::TextEditor; @@ -135,5 +130,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/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477..d7235cf6 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -232,7 +232,7 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::Idle) + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { interaction diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 8a4d6a98..98efe305 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,5 +1,4 @@ //! Build and show dropdown menus. -use crate::container::{self, Container}; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; @@ -10,24 +9,25 @@ 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< 'a, + 'b, T, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, + 'b: 'a, { state: &'a mut State, options: &'a [T], @@ -40,24 +40,27 @@ pub struct Menu< text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> +impl<'a, 'b, T, Message, Theme, Renderer> + Menu<'a, 'b, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, + 'b: '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>, + class: &'a <Theme as Catalog>::Class<'b>, ) -> Self { Menu { state, @@ -71,7 +74,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + class, } } @@ -114,15 +117,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. /// @@ -163,28 +157,29 @@ impl Default for State { } } -struct Overlay<'a, Message, Theme, Renderer> +struct Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, + Theme: Catalog, Renderer: crate::core::Renderer, { position: Point, state: &'a mut Tree, - container: Container<'a, Message, Theme, Renderer>, + list: Scrollable<'a, Message, Theme, Renderer>, width: f32, target_height: f32, - style: <Theme as StyleSheet>::Style, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> +impl<'a, 'b, Message, Theme, Renderer> Overlay<'a, 'b, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: Catalog + scrollable::Catalog + 'a, Renderer: text::Renderer + 'a, + 'b: 'a, { pub fn new<T>( position: Point, - menu: Menu<'a, T, Message, Theme, Renderer>, + menu: Menu<'a, 'b, T, Message, Theme, Renderer>, target_height: f32, ) -> Self where @@ -202,40 +197,43 @@ where text_size, text_line_height, text_shaping, - style, + class, } = 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 list = Scrollable::with_direction( + List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + class, + }, + scrollable::Direction::default(), + ); - state.tree.diff(&container as &dyn Widget<_, _, _>); + state.tree.diff(&list as &dyn Widget<_, _, _>); Self { position, state: &mut state.tree, - container, + list, width, target_height, - style, + class, } } } -impl<'a, Message, Theme, Renderer> +impl<'a, 'b, Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer> - for Overlay<'a, Message, Theme, Renderer> + for Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -256,7 +254,7 @@ where ) .width(self.width); - let node = self.container.layout(self.state, renderer, &limits); + let node = self.list.layout(self.state, renderer, &limits); let size = node.size(); node.move_to(if space_below > space_above { @@ -277,7 +275,7 @@ where ) -> event::Status { let bounds = layout.bounds(); - self.container.on_event( + self.list.on_event( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) @@ -290,7 +288,7 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.container + self.list .mouse_interaction(self.state, layout, cursor, viewport, renderer) } @@ -298,30 +296,32 @@ where &self, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { - let appearance = StyleSheet::appearance(theme, &self.style); let bounds = layout.bounds(); + let style = Catalog::style(theme, self.class); + renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); - self.container - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); + self.list.draw( + self.state, renderer, theme, defaults, layout, cursor, &bounds, + ); } } -struct List<'a, T, Message, Theme, Renderer> +struct List<'a, 'b, T, Message, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { options: &'a [T], @@ -333,14 +333,14 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for List<'a, T, Message, Theme, Renderer> +impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for List<'a, 'b, T, Message, Theme, Renderer> where T: Clone + ToString, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -483,7 +483,7 @@ where _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = theme.appearance(&self.style); + let style = Catalog::style(theme, self.class); let bounds = layout.bounds(); let text_size = @@ -513,20 +513,20 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + appearance.border.width, - width: bounds.width - appearance.border.width * 2.0, + x: bounds.x + style.border.width, + width: bounds.width - style.border.width * 2.0, ..bounds }, - border: Border::with_radius(appearance.border.radius), + border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, - appearance.selected_background, + style.selected_background, ); } renderer.fill_text( Text { - content: &option.to_string(), + content: option.to_string(), bounds: Size::new(f32::INFINITY, bounds.height), size: text_size, line_height: self.text_line_height, @@ -537,9 +537,9 @@ where }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { - appearance.selected_text_color + style.selected_text_color } else { - appearance.text_color + style.text_color }, *viewport, ); @@ -547,16 +547,81 @@ where } } -impl<'a, T, Message, Theme, Renderer> - From<List<'a, T, Message, Theme, Renderer>> +impl<'a, 'b, T, Message, Theme, Renderer> + From<List<'a, 'b, T, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a + Catalog, Renderer: 'a + text::Renderer, + 'b: 'a, { - fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { + fn from(list: List<'a, 'b, T, Message, Theme, Renderer>) -> Self { Element::new(list) } } + +/// The appearance of a [`Menu`]. +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// 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 theme catalog of a [`Menu`]. +pub trait Catalog: scrollable::Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; + + /// The default class for the scrollable of the [`Menu`]. + fn default_scrollable<'a>() -> <Self as scrollable::Catalog>::Class<'a> { + <Self as scrollable::Catalog>::default() + } + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style; +} + +/// A styling function for a [`Menu`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } + + fn style(&self, class: &StyleFn<'_, Self>) -> Style { + class(self) + } +} + +/// The default style of the list of a [`Menu`]. +pub fn default(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + 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 478a7024..acfa9d44 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -30,8 +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; @@ -42,8 +40,8 @@ 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, + self, Background, Border, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; const DRAG_DEADBAND_DISTANCE: f32 = 10.0; @@ -73,8 +71,7 @@ const THICKNESS_RATIO: f32 = 25.0; /// ```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, @@ -105,8 +102,8 @@ pub struct PaneGrid< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, width: Length, @@ -115,13 +112,13 @@ 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, + class: <Theme as Catalog>::Class<'a>, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. /// @@ -161,7 +158,7 @@ where on_click: None, on_drag: None, on_resize: None, - style: Default::default(), + class: <Theme as Catalog>::default(), } } @@ -221,11 +218,23 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style( + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`PaneGrid`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class( mut self, - style: impl Into<<Theme as StyleSheet>::Style>, + class: impl Into<<Theme as Catalog>::Class<'a>>, ) -> Self { - self.style = style.into(); + self.class = class.into(); self } @@ -239,8 +248,8 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for PaneGrid<'a, Message, Theme, Renderer> where - Renderer: crate::core::Renderer, - Theme: StyleSheet + container::StyleSheet, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<state::Action>() @@ -285,19 +294,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( @@ -329,7 +348,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 @@ -337,19 +359,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); @@ -383,32 +550,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( @@ -416,33 +612,215 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, 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 style = Catalog::style(theme, &self.class); + + 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, + defaults, + 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: style.hovered_region.border, + ..renderer::Quad::default() + }, + style.hovered_region.background, + ); + } + } + } + _ => { + content.draw( + tree, + renderer, + theme, + defaults, + 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: style.hovered_region.border, + ..renderer::Quad::default() + }, + style.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, + defaults, + layout, + pane_cursor, + viewport, + ); + }); + }); + } + } + + if picked_pane.is_none() { + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + style.picked_split + } else { + style.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>( @@ -470,8 +848,8 @@ 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, - Renderer: crate::core::Renderer + 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( pane_grid: PaneGrid<'a, Message, Theme, Renderer>, @@ -480,219 +858,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 { - 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, - on_drag, - ); - } - } - None => { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - 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 = 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((_, 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(); @@ -748,257 +913,6 @@ fn click_pane<'a, Message, T>( } } -/// 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.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().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(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( - pane, - renderer, - default_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); - 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(origin.x, 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, - ); - } - } - } -} - fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> { let bounds = layout.bounds(); @@ -1215,3 +1129,90 @@ impl<'a, T> Contents<'a, T> { matches!(self, Self::Maximized(..)) } } + +/// The appearance of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// 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 theme catalog of a [`PaneGrid`]. +pub trait Catalog: container::Catalog { + /// The item class of this [`Catalog`]. + type Class<'a>; + + /// The default class produced by this [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style; +} + +/// A styling function for a [`PaneGrid`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } + + fn style(&self, class: &StyleFn<'_, Self>) -> Style { + class(self) + } +} + +/// The default style of a [`PaneGrid`]. +pub fn default(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + 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 dfe0fdcf..30ad52ca 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -6,7 +6,7 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, }; use crate::pane_grid::{Draggable, TitleBar}; @@ -20,29 +20,29 @@ pub struct Content< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>, body: Element<'a, Message, Theme, Renderer>, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { /// Creates a new [`Content`] with the provided body. pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self { Self { title_bar: None, body: body.into(), - style: Default::default(), + class: Theme::default(), } } - /// Sets the [`TitleBar`] of this [`Content`]. + /// Sets the [`TitleBar`] of the [`Content`]. pub fn title_bar( mut self, title_bar: TitleBar<'a, Message, Theme, Renderer>, @@ -52,16 +52,31 @@ where } /// Sets the style of the [`Content`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style( + mut self, + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Content`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { let children = if let Some(title_bar) = self.title_bar.as_ref() { @@ -90,7 +105,7 @@ where /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::core::Renderer + /// [`Renderer`]: core::Renderer pub fn draw( &self, tree: &Tree, @@ -104,7 +119,7 @@ where let bounds = layout.bounds(); { - let style = theme.appearance(&self.style); + let style = theme.style(&self.class); container::draw_background(renderer, &style, bounds); } @@ -370,8 +385,8 @@ where impl<'a, Message, Theme, Renderer> Draggable for &Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { fn can_be_dragged_at( &self, @@ -393,8 +408,8 @@ 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, - Renderer: crate::core::Renderer, + Theme: container::Catalog + 'a, + Renderer: core::Renderer, { fn from(element: T) -> Self { Self::new(element) diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 481cd770..c20c3b9c 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -6,7 +6,7 @@ use crate::pane_grid::{ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; -use std::collections::HashMap; +use rustc_hash::FxHashMap; /// The state of a [`PaneGrid`]. /// @@ -25,7 +25,7 @@ pub struct State<T> { /// The panes of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid - pub panes: HashMap<Pane, T>, + pub panes: FxHashMap<Pane, T>, /// The internal state of the [`PaneGrid`]. /// @@ -52,7 +52,7 @@ impl<T> State<T> { /// Creates a new [`State`] with the given [`Configuration`]. pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { - let mut panes = HashMap::new(); + let mut panes = FxHashMap::default(); let internal = Internal::from_configuration(&mut panes, config.into(), 0); @@ -353,7 +353,7 @@ impl Internal { /// /// [`PaneGrid`]: super::PaneGrid pub fn from_configuration<T>( - panes: &mut HashMap<Pane, T>, + panes: &mut FxHashMap<Pane, T>, content: Configuration<T>, next_id: usize, ) -> Self { diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 5b57509b..c2eeebb7 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -6,7 +6,8 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, + Vector, }; /// The title bar of a [`Pane`]. @@ -19,32 +20,31 @@ pub struct TitleBar< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { content: Element<'a, Message, Theme, Renderer>, controls: Option<Element<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { /// Creates a new [`TitleBar`] with the given content. - pub fn new<E>(content: E) -> Self - where - E: Into<Element<'a, Message, Theme, Renderer>>, - { + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { Self { content: content.into(), controls: None, padding: Padding::ZERO, always_show_controls: false, - style: Default::default(), + class: Theme::default(), } } @@ -63,12 +63,6 @@ where self } - /// Sets the style of the [`TitleBar`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); - self - } - /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are /// always visible. /// @@ -81,12 +75,33 @@ where self.always_show_controls = true; self } + + /// Sets the style of the [`TitleBar`]. + #[must_use] + pub fn style( + mut self, + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TitleBar`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, - Renderer: crate::core::Renderer, + Theme: container::Catalog, + Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { let children = if let Some(controls) = self.controls.as_ref() { @@ -115,7 +130,7 @@ where /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. /// - /// [`Renderer`]: crate::core::Renderer + /// [`Renderer`]: core::Renderer pub fn draw( &self, tree: &Tree, @@ -128,7 +143,8 @@ where show_controls: bool, ) { let bounds = layout.bounds(); - let style = theme.appearance(&self.style); + let style = theme.style(&self.class); + 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 1f20e2bc..edccfdaa 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,15 +10,13 @@ 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::Borrow; - -pub use crate::style::pick_list::{Appearance, StyleSheet}; +use std::f32; /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] @@ -35,7 +32,7 @@ pub struct PickList< T: ToString + PartialEq + Clone, L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { on_select: Box<dyn Fn(T) -> Message + 'a>, @@ -51,7 +48,8 @@ pub struct PickList< text_shaping: text::Shaping, font: Option<Renderer::Font>, handle: Handle<Renderer::Font>, - style: Theme::Style, + class: <Theme as Catalog>::Class<'a>, + menu_class: <Theme as menu::Catalog>::Class<'a>, } impl<'a, T, L, V, Message, Theme, Renderer> @@ -61,16 +59,9 @@ where 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>, + Theme: Catalog, 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( @@ -86,13 +77,14 @@ where 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(), + class: <Theme as Catalog>::default(), + menu_class: <Theme as Catalog>::default_menu(), } } @@ -160,11 +152,23 @@ where } /// Sets the style of the [`PickList`]. - pub fn style( + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`PickList`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class( mut self, - style: impl Into<<Theme as StyleSheet>::Style>, + class: impl Into<<Theme as Catalog>::Class<'a>>, ) -> Self { - self.style = style.into(); + self.class = class.into(); self } } @@ -176,11 +180,7 @@ where 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>, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn tag(&self) -> tree::Tag { @@ -204,19 +204,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.borrow(), - ) + 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( @@ -230,18 +288,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().map(Borrow::borrow), - self.options.borrow(), - || 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( @@ -252,7 +390,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( @@ -266,23 +411,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().map(Borrow::borrow), - &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 style = Catalog::style(theme, &self.class, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: style.border, + ..renderer::Quad::default() + }, + style.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(), + ), + style.handle_color, + *viewport, + ); + } + + let label = selected.map(ToString::to_string); + + if let Some(label) = label.or_else(|| self.placeholder.clone()) { + 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 { + style.text_color + } else { + style.placeholder_color + }, + *viewport, + ); + } } fn overlay<'b>( @@ -293,19 +539,38 @@ 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()); - overlay( - layout, - translation, - state, - self.padding, - self.text_size, - self.text_shaping, - self.font.unwrap_or_else(|| renderer.default_font()), - self.options.borrow(), - &self.on_select, - self.style.clone(), - ) + if state.is_open { + let bounds = layout.bounds(); + + 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.menu_class, + ) + .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 + } } } @@ -317,12 +582,7 @@ where 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: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -332,9 +592,8 @@ where } } -/// 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, @@ -407,394 +666,94 @@ 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(); +/// The appearance of a pick list. +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// 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, +} - 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; +/// The theme catalog of a [`PickList`]. +pub trait Catalog: menu::Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; - if let Some(on_close) = on_close { - shell.publish(on_close.clone()); - } + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> <Self as Catalog>::Class<'a>; - event::Status::Captured - } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = - options.iter().position(|option| Some(option) == selected); + /// The default class for the menu of the [`PickList`]. + fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> { + <Self as menu::Catalog>::default() + } - if let Some(on_open) = on_open { - shell.publish(on_open.clone()); - } + /// The [`Style`] of a class with the given status. + fn style( + &self, + class: &<Self as Catalog>::Class<'_>, + status: Status, + ) -> Style; +} - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) => { - let state = state(); +/// A styling function for a [`PickList`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; - 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); +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; - options.next() - } + fn default<'a>() -> StyleFn<'a, Self> { + Box::new(default) + } - 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(); - - state.keyboard_modifiers = modifiers; - - event::Status::Ignored - } - _ => event::Status::Ignored, + fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style { + class(self, status) } } -/// 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); +/// The default style of the field of a [`PickList`]. +pub fn default(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// 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); - } - - 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() + let active = Style { + 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, + match status { + Status::Active => active, + Status::Hovered | Status::Opened => Style { + border: Border { + color: palette.primary.strong.color, + ..active.border }, - 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 - }, - *viewport, - ); + ..active + }, } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 694fdd28..e7821b43 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -3,17 +3,18 @@ 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::{ + self, 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,20 +23,20 @@ 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> +pub struct ProgressBar<'a, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Theme::Style, + class: Theme::Class<'a>, } -impl<Theme> ProgressBar<Theme> +impl<'a, Theme> ProgressBar<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: f32 = 30.0; @@ -51,7 +52,7 @@ where range, width: Length::Fill, height: None, - style: Default::default(), + class: Theme::default(), } } @@ -68,17 +69,29 @@ where } /// Sets the style of the [`ProgressBar`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`ProgressBar`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); 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, + Theme: Catalog, + Renderer: core::Renderer, { fn size(&self) -> Size<Length> { Size { @@ -120,12 +133,12 @@ where / (range_end - range_start) }; - let style = theme.appearance(&self.style); + let style = theme.style(&self.class); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border: Border::with_radius(style.border_radius), + border: style.border, ..renderer::Quad::default() }, style.background, @@ -138,7 +151,7 @@ where width: active_progress_width, ..bounds }, - border: Border::with_radius(style.border_radius), + border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, style.bar, @@ -147,16 +160,101 @@ where } } -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, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + 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 Style { + /// 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 theme catalog of a [`ProgressBar`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`ProgressBar`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(primary) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// The primary style of a [`ProgressBar`]. +pub fn primary(theme: &Theme) -> Style { + 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) -> Style { + 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) -> Style { + 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) -> Style { + let palette = theme.extended_palette(); + + styled(palette.background.strong.color, palette.danger.base.color) +} + +fn styled( + background: impl Into<Background>, + bar: impl Into<Background>, +) -> Style { + Style { + 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 eeb1526f..e064aada 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -5,41 +5,39 @@ 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; 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)] +#[allow(missing_debug_implementations)] pub struct QRCode<'a, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { data: &'a Data, cell_size: u16, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Theme> QRCode<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { /// Creates a new [`QRCode`] with the provided [`Data`]. pub fn new(data: &'a Data) -> Self { Self { data, cell_size: DEFAULT_CELL_SIZE, - style: Default::default(), + class: Theme::default(), } } @@ -50,15 +48,27 @@ where } /// Sets the style of the [`QRCode`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`QRCode`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -102,13 +112,13 @@ where let bounds = layout.bounds(); let side_length = self.data.width + 2 * QUIET_ZONE; - let appearance = theme.appearance(&self.style); - let mut last_appearance = state.last_appearance.borrow_mut(); + let style = theme.style(&self.class); + let mut last_style = state.last_style.borrow_mut(); - if Some(appearance) != *last_appearance { + if Some(style) != *last_style { self.data.cache.clear(); - *last_appearance = Some(appearance); + *last_style = Some(style); } // Reuse cache if possible @@ -120,7 +130,7 @@ where frame.fill_rectangle( Point::ORIGIN, Size::new(side_length as f32, side_length as f32), - appearance.background, + style.background, ); // Avoid drawing on the quiet zone @@ -139,7 +149,7 @@ where frame.fill_rectangle( Point::new(column as f32, row as f32), Size::UNIT, - appearance.cell, + style.cell, ); }); }); @@ -147,7 +157,9 @@ where renderer.with_translation( bounds.position() - Point::ORIGIN, |renderer| { - renderer.draw(vec![geometry]); + use crate::graphics::geometry::Renderer as _; + + renderer.draw_geometry(geometry); }, ); } @@ -156,7 +168,7 @@ where impl<'a, Message, Theme> From<QRCode<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: Catalog + 'a, { fn from(qr_code: QRCode<'a, Theme>) -> Self { Self::new(qr_code) @@ -170,7 +182,7 @@ where pub struct Data { contents: Vec<qrcode::Color>, width: usize, - cache: canvas::Cache, + cache: canvas::Cache<Renderer>, } impl Data { @@ -328,5 +340,51 @@ impl From<qrcode::types::QrError> for Error { #[derive(Default)] struct State { - last_appearance: RefCell<Option<Appearance>>, + last_style: RefCell<Option<Style>>, +} + +/// The appearance of a QR code. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The color of the QR code data cells + pub cell: Color, + /// The color of the QR code background + pub background: Color, +} + +/// The theme catalog of a [`QRCode`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`QRCode`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// The default style of a [`QRCode`]. +pub fn default(theme: &Theme) -> Style { + let palette = theme.palette(); + + Style { + cell: palette.text, + background: palette.background, + } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 68e9bc7e..6b22961d 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,9 @@ 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, + Theme: Catalog, Renderer: text::Renderer, { is_selected: bool, @@ -84,20 +82,20 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + class: Theme::Class<'a>, } -impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, + Theme: Catalog, 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. /// @@ -128,7 +126,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + class: Theme::default(), } } @@ -178,17 +176,29 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Radio`] button. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); 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, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -285,21 +295,24 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, 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 style = theme.style(&self.class, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -312,12 +325,12 @@ where bounds, border: Border { radius: (size / 2.0).into(), - width: custom_style.border_width, - color: custom_style.border_color, + width: style.border_width, + color: style.border_color, }, ..renderer::Quad::default() }, - custom_style.background, + style.background, ); if self.is_selected { @@ -329,10 +342,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, + style.dot_color, ); } } @@ -342,11 +355,11 @@ where crate::text::draw( renderer, - style, + defaults, label_layout, tree.state.downcast_ref(), - crate::text::Appearance { - color: custom_style.text_color, + crate::text::Style { + color: style.text_color, }, viewport, ); @@ -354,16 +367,95 @@ 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 + Catalog, 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 Style { + /// 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 theme catalog of a [`Radio`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Radio`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`Radio`] button. +pub fn default(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + + let active = Style { + 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 { .. } => Style { + 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 47feff9c..fa352171 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -31,11 +31,18 @@ where Self::from_vec(Vec::new()) } + /// Creates a [`Row`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + /// Creates a [`Row`] with the given elements. pub fn with_children( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Self { - Self::new().extend(children) + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) } /// Creates a [`Row`] from an already allocated [`Vec`]. diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bca34541..1a536d2f 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,29 +1,29 @@ //! Display a horizontal or vertical rule for dividing content. +use crate::core; +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> +pub struct Rule<'a, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { width: Length, height: Length, is_horizontal: bool, - style: Theme::Style, + class: Theme::Class<'a>, } -impl<Theme> Rule<Theme> +impl<'a, Theme> Rule<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { /// Creates a horizontal [`Rule`] with the given height. pub fn horizontal(height: impl Into<Pixels>) -> Self { @@ -31,7 +31,7 @@ where width: Length::Fill, height: Length::Fixed(height.into().0), is_horizontal: true, - style: Default::default(), + class: Theme::default(), } } @@ -41,21 +41,34 @@ where width: Length::Fixed(width.into().0), height: Length::Fill, is_horizontal: false, - style: Default::default(), + class: Theme::default(), } } /// Sets the style of the [`Rule`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Rule`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); 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, + Renderer: core::Renderer, + Theme: Catalog, { fn size(&self) -> Size<Length> { Size { @@ -84,7 +97,7 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let style = theme.appearance(&self.style); + let style = theme.style(&self.class); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) @@ -119,7 +132,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::with_radius(style.radius), + border: Border::rounded(style.radius), ..renderer::Quad::default() }, style.color, @@ -127,14 +140,132 @@ where } } -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, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + 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 Style { + /// The color of the rule. + pub color: Color, + /// The width (thickness) of the rule line. + pub width: u16, + /// The radius of the line corners. + pub radius: 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 theme catalog of a [`Rule`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>) -> Style; +} + +/// A styling function for a [`Rule`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class(self) + } +} + +/// The default styling of a [`Rule`]. +pub fn default(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + Style { + 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 f736d92e..6fc00f87 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,5 @@ //! 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; @@ -11,14 +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, + self, Background, Border, Clipboard, Color, Element, Layout, Length, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use crate::style::scrollable::{ - Appearance, Scrollbar, Scroller, StyleSheet, -}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a @@ -30,8 +28,8 @@ pub struct Scrollable< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { id: Option<Id>, width: Length, @@ -39,13 +37,13 @@ pub struct Scrollable< direction: Direction, content: Element<'a, Message, Theme, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> where - Theme: StyleSheet, - Renderer: crate::core::Renderer, + Theme: Catalog, + Renderer: core::Renderer, { /// Creates a new vertical [`Scrollable`]. pub fn new( @@ -80,7 +78,7 @@ where direction, content, on_scroll: None, - style: Default::default(), + class: Theme::default(), } } @@ -110,9 +108,21 @@ where self } - /// Sets the style of the [`Scrollable`] . - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + /// Sets the style of this [`Scrollable`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Scrollable`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -223,8 +233,8 @@ 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, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -255,20 +265,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( @@ -318,28 +337,315 @@ 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); + + 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, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return 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); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return 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, + ); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return 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); + + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + } + _ => {} + } + } + + 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 !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed + && 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); + + event_status = if notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ) { + event::Status::Captured + } else { + event::Status::Ignored + }; + } + 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); + + // TODO: bubble up touch movements if not consumed. + let _ = notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + _ => {} + } + + event_status = event::Status::Captured; + } + _ => {} + } + + event_status } fn draw( @@ -347,31 +653,186 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { - draw( - tree.state.downcast_ref::<State>(), - 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, + 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 Some(visible_bounds) = bounds.intersection(viewport) else { + return; + }; + + 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 style = theme.style(&self.class, status); + + container::draw_background(renderer, &style.container, layout.bounds()); + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(visible_bounds, |renderer| { + renderer.with_translation( + Vector::new(-translation.x, -translation.y), + |renderer| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + defaults, + 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: (visible_bounds.width + 2.0).min(viewport.width), + height: (visible_bounds.height + 2.0).min(viewport.height), + ..visible_bounds + }, + |renderer| { + if let Some(scrollbar) = scrollbars.y { + draw_scrollbar( + renderer, + style.vertical_scrollbar, + &scrollbar, + ); + } + + if let Some(scrollbar) = scrollbars.x { + draw_scrollbar( + renderer, + style.horizontal_scrollbar, + &scrollbar, + ); + } + + if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) { + let background = + style.gap.or(style.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, + defaults, + content_layout, + cursor, + &Rectangle { + x: bounds.x + translation.x, + y: bounds.y + translation.y, + ..bounds + }, + ); + } } fn mouse_interaction( @@ -382,21 +843,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::None + } 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>( @@ -430,8 +918,8 @@ impl<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + 'a, - Renderer: 'a + crate::core::Renderer, + Theme: 'a + Catalog, + Renderer: 'a + core::Renderer, { fn from( text_input: Scrollable<'a, Message, Theme, Renderer>, @@ -482,620 +970,58 @@ 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, - }; - - let appearance = if state.y_scroller_grabbed_at.is_some() - || state.x_scroller_grabbed_at.is_some() - { - theme.dragging(style) - } else if cursor_over_scrollable.is_some() { - theme.hovered(style, mouse_over_y_scrollbar || mouse_over_x_scrollbar) - } else { - theme.active(style) - }; - - let scrollbar_style = |is_dragging: bool, mouse_over_scrollbar: bool| { - if is_dragging { - theme.dragging(style).scrollbar - } else if cursor_over_scrollable.is_some() { - theme.hovered(style, mouse_over_scrollbar).scrollbar - } else { - theme.active(style).scrollbar - } - }; - - container::draw_background( - renderer, - &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| { - 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| { - 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, - scrollbar_style( - state.y_scroller_grabbed_at.is_some(), - mouse_over_y_scrollbar, - ), - &scrollbar, - ); - } - - if let Some(scrollbar) = scrollbars.x { - draw_scrollbar( - renderer, - scrollbar_style( - state.x_scroller_grabbed_at.is_some(), - mouse_over_x_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 { - draw_content( - renderer, - content_layout, - cursor, - &Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..bounds - }, - ); - } -} - +/// Returns [`true`] if the viewport actually changed. fn notify_on_scroll<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } +) -> bool { + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return false; + } - let viewport = Viewport { - offset_x: state.offset_x, - offset_y: state.offset_y, - bounds, - content_bounds, + let viewport = Viewport { + offset_x: state.offset_x, + offset_y: state.offset_y, + bounds, + content_bounds, + }; + + // Don't publish redundant viewports to shell + if let Some(last_notified) = state.last_notified { + let last_relative_offset = last_notified.relative_offset(); + let current_relative_offset = viewport.relative_offset(); + + let last_absolute_offset = last_notified.absolute_offset(); + let current_absolute_offset = viewport.absolute_offset(); + + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) }; - // Don't publish redundant viewports to shell - if let Some(last_notified) = state.last_notified { - let last_relative_offset = last_notified.relative_offset(); - let current_relative_offset = viewport.relative_offset(); - - let last_absolute_offset = last_notified.absolute_offset(); - let current_absolute_offset = viewport.absolute_offset(); - - let unchanged = |a: f32, b: f32| { - (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) - }; - - if unchanged(last_relative_offset.x, current_relative_offset.x) - && unchanged(last_relative_offset.y, current_relative_offset.y) - && unchanged(last_absolute_offset.x, current_absolute_offset.x) - && unchanged(last_absolute_offset.y, current_absolute_offset.y) - { - return; - } + if unchanged(last_relative_offset.x, current_relative_offset.x) + && unchanged(last_relative_offset.y, current_relative_offset.y) + && unchanged(last_absolute_offset.x, current_absolute_offset.x) + && unchanged(last_absolute_offset.y, current_absolute_offset.y) + { + return false; } - - shell.publish(on_scroll(viewport)); - state.last_notified = Some(viewport); } + + if let Some(on_scroll) = on_scroll { + shell.publish(on_scroll(viewport)); + } + state.last_notified = Some(viewport); + + true } -/// 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>, @@ -1625,3 +1551,161 @@ 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 Style { + /// The [`container::Style`] of a scrollable. + pub container: container::Style, + /// 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 theme catalog of a [`Scrollable`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Scrollable`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`Scrollable`]. +pub fn default(theme: &Theme, status: Status) -> Style { + 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 => Style { + container: container::Style::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 + }; + + Style { + container: container::Style::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 + }; + + Style { + container: container::Style::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/shader.rs b/widget/src/shader.rs index 68112f83..fad2f4eb 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -13,12 +13,13 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::primitive; use std::marker::PhantomData; +pub use crate::graphics::Viewport; pub use crate::renderer::wgpu::wgpu; -pub use pipeline::{Primitive, Storage}; +pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. /// @@ -60,7 +61,7 @@ impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Shader<Message, P> where P: Program<Message>, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, { fn tag(&self) -> tree::Tag { struct Tag<T>(T); @@ -160,7 +161,7 @@ where let bounds = layout.bounds(); let state = tree.state.downcast_ref::<P::State>(); - renderer.draw_pipeline_primitive( + renderer.draw_primitive( bounds, self.program.draw(state, cursor_position, bounds), ); @@ -171,7 +172,7 @@ impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, P: Program<Message> + 'a, { fn from( diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 6dd50404..902c7c3b 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,7 +1,7 @@ use crate::core::event; use crate::core::mouse; use crate::core::{Rectangle, Shell}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::Primitive; use crate::shader; /// The state and logic of a [`Shader`] widget. @@ -15,7 +15,7 @@ pub trait Program<Message> { type State: Default + 'static; /// The type of primitive this [`Program`] can draw. - type Primitive: pipeline::Primitive + 'static; + type Primitive: Primitive + 'static; /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 65bc1772..a8f1d192 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, + self, 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 { @@ -47,7 +41,7 @@ pub use iced_style::slider::{ #[allow(missing_debug_implementations)] pub struct Slider<'a, T, Message, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { range: RangeInclusive<T>, step: T, @@ -58,17 +52,17 @@ where on_release: Option<Message>, width: Length, height: f32, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + From<u8> + PartialOrd, Message: Clone, - Theme: StyleSheet, + Theme: Catalog, { /// The default height of a [`Slider`]. - pub const DEFAULT_HEIGHT: f32 = 22.0; + pub const DEFAULT_HEIGHT: f32 = 16.0; /// Creates a new [`Slider`]. /// @@ -104,7 +98,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Default::default(), + class: Theme::default(), } } @@ -139,12 +133,6 @@ where self } - /// Sets the style of the [`Slider`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); - self - } - /// Sets the step size of the [`Slider`]. pub fn step(mut self, step: impl Into<T>) -> Self { self.step = step.into(); @@ -158,6 +146,24 @@ where self.shift_step = Some(shift_step.into()); self } + + /// Sets the style of the [`Slider`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Slider`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -165,15 +171,15 @@ 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, + Theme: Catalog, + Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -203,20 +209,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 +358,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, - theme, - &self.style, + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = theme.style( + &self.class, + 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 +455,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,8 +474,8 @@ 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, - Renderer: crate::core::Renderer + 'a, + Theme: Catalog + 'a, + Renderer: core::Renderer + 'a, { fn from( slider: Slider<'a, T, Message, Theme>, @@ -268,290 +484,132 @@ 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(); - - 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() { - 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; - } - }; - - 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; - } - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = on_release.clone() { - shell.publish(on_release); - } - state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - 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 -} - -/// 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(); - - (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::with_radius(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::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() - }, - 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 { +struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, } -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() +/// 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, +} + +/// The appearance of a slider. +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// The colors of the rail of the slider. + pub rail: Rail, + /// The appearance of the [`Handle`] of the slider. + pub handle: Handle, +} + +impl Style { + /// Changes the [`HandleShape`] of the [`Style`] 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 + } +} + +/// 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, + }, +} + +/// The theme catalog of a [`Slider`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Slider`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`Slider`]. +pub fn default(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + + let color = match status { + Status::Active => palette.primary.strong.color, + Status::Hovered => palette.primary.base.color, + Status::Dragged => palette.primary.strong.color, + }; + + Style { + rail: Rail { + colors: (color, palette.secondary.base.color), + width: 4.0, + border_radius: 2.0.into(), + }, + handle: Handle { + shape: HandleShape::Circle { radius: 7.0 }, + color, + border_color: Color::TRANSPARENT, + border_width: 0.0, + }, } } diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 00000000..5035541b --- /dev/null +++ b/widget/src/stack.rs @@ -0,0 +1,333 @@ +//! Display content on top of other content. +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::widget::{Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, +}; + +/// A container that displays children on top of each other. +/// +/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and +/// will be displayed as the base layer. Every consecutive [`Element`] will be +/// renderer on top; on its own layer. +/// +/// Keep in mind that too much layering will normally produce bad UX as well as +/// introduce certain rendering overhead. Use this widget sparingly! +#[allow(missing_debug_implementations)] +pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +{ + width: Length, + height: Length, + children: Vec<Element<'a, Message, Theme, Renderer>>, +} + +impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Creates an empty [`Stack`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Stack`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Stack`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Stack`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Stack`] 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 [`Stack::width`] or [`Stack::height`] accordingly. + pub fn from_vec( + children: Vec<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + width: Length::Shrink, + height: Length::Shrink, + children, + } + } + + /// Sets the width of the [`Stack`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Stack`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Adds an element to the [`Stack`]. + pub fn push( + mut self, + child: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + let child = child.into(); + + if self.children.is_empty() { + let child_size = child.as_widget().size_hint(); + + 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 [`Stack`], 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 [`Stack`] 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 Stack<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + if self.children.is_empty() { + return layout::Node::new(limits.resolve( + self.width, + self.height, + Size::ZERO, + )); + } + + let base = self.children[0].as_widget().layout( + &mut tree.children[0], + renderer, + &limits, + ); + + let size = limits.resolve(self.width, self.height, base.size()); + let limits = layout::Limits::new(Size::ZERO, size); + + let nodes = std::iter::once(base) + .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map( + |(layer, tree)| { + let node = + layer.as_widget().layout(tree, renderer, &limits); + + node + }, + )) + .collect(); + + layout::Node::with_children(size, nodes) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .rev() + .zip(tree.children.iter_mut().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .find(|&status| status == event::Status::Captured) + .unwrap_or(event::Status::Ignored) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::None) + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for (i, ((layer, state), layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .enumerate() + { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + } + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From<Stack<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self { + Self::new(stack) + } +} diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 12ef3d92..4551bcad 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, Point, Rectangle, Rotation, + 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,20 +20,22 @@ 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> +pub struct Svg<'a, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { handle: Handle, width: Length, height: Length, content_fit: ContentFit, - style: <Theme as StyleSheet>::Style, + class: Theme::Class<'a>, + rotation: Rotation, + opacity: f32, } -impl<Theme> Svg<Theme> +impl<'a, Theme> Svg<'a, Theme> where - Theme: StyleSheet, + Theme: Catalog, { /// Creates a new [`Svg`] from the given [`Handle`]. pub fn new(handle: impl Into<Handle>) -> Self { @@ -42,7 +44,9 @@ where width: Length::Fill, height: Length::Shrink, content_fit: ContentFit::Contain, - style: Default::default(), + class: Theme::default(), + rotation: Rotation::default(), + opacity: 1.0, } } @@ -78,18 +82,45 @@ where } } - /// Sets the style variant of this [`Svg`]. + /// Sets the style of the [`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) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Svg`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } + + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Svg`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into<f32>) -> Self { + self.opacity = opacity.into(); 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, + Theme: Catalog, { fn size(&self) -> Size<Length> { Size { @@ -105,14 +136,17 @@ where limits: &layout::Limits, ) -> layout::Node { // The raw w/h of the underlying image - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + // The rotated size of the svg + let rotated_size = self.rotation.apply(image_size); + // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(self.width, self.height, image_size); + let raw_size = limits.resolve(self.width, self.height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -139,35 +173,49 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Vector::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + + let final_size = image_size * scale; + + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); + let is_mouse_over = cursor.is_over(bounds); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + + let style = theme.style(&self.class, status); + let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - let appearance = if is_mouse_over { - theme.hovered(&self.style) - } else { - theme.appearance(&self.style) - }; - - renderer.draw( + renderer.draw_svg( self.handle.clone(), - appearance.color, - drawing_bounds + offset, + style.color, + drawing_bounds, + self.rotation.radians(), + self.opacity, ); }; @@ -181,13 +229,68 @@ 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: Catalog + '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 Style { + /// The [`Color`] filter of an [`Svg`]. + /// + /// Useful for coloring a symbolic icon. + /// + /// `None` keeps the original color. + pub color: Option<Color>, +} + +/// The theme catalog of an [`Svg`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|_theme, _status| Style::default()) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// A styling function for an [`Svg`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl<'a, Theme> From<Style> for StyleFn<'a, Theme> { + fn from(style: Style) -> Self { + Box::new(move |_theme, _status| style) + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index bad3ef4d..7c0b98ea 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -11,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::{ - Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, + Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, + Shell, Size, Theme, Vector, }; use std::cell::RefCell; @@ -19,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. @@ -32,7 +32,7 @@ pub struct TextEditor< Renderer = crate::Renderer, > where Highlighter: text::Highlighter, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { content: &'a Content<Renderer>, @@ -42,7 +42,7 @@ pub struct TextEditor< width: Length, height: Length, padding: Padding, - style: Theme::Style, + class: Theme::Class<'a>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -54,7 +54,7 @@ pub struct TextEditor< impl<'a, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { /// Creates new [`TextEditor`] with the given [`Content`]. @@ -67,7 +67,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), - style: Default::default(), + class: Theme::default(), on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -81,7 +81,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { /// Sets the height of the [`TextEditor`]. @@ -110,6 +110,21 @@ where self } + /// Sets the text size of the [`TextEditor`]. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.text_size = Some(size.into()); + self + } + + /// Sets the [`text::LineHeight`] of the [`TextEditor`]. + pub fn line_height( + mut self, + line_height: impl Into<text::LineHeight>, + ) -> Self { + self.line_height = line_height.into(); + self + } + /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into<Padding>) -> Self { self.padding = padding.into(); @@ -134,7 +149,7 @@ where width: self.width, height: self.height, padding: self.padding, - style: self.style, + class: self.class, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, @@ -142,8 +157,20 @@ where } /// Sets the style of the [`TextEditor`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TextEditor`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -292,7 +319,9 @@ where } } -struct State<Highlighter: text::Highlighter> { +/// The state of a [`TextEditor`]. +#[derive(Debug)] +pub struct State<Highlighter: text::Highlighter> { is_focused: bool, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, @@ -302,11 +331,18 @@ struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } +impl<Highlighter: text::Highlighter> State<Highlighter> { + /// Returns whether the [`TextEditor`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.is_focused + } +} + impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> widget::tree::Tag { @@ -477,7 +513,7 @@ where tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, - style: &renderer::Style, + defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -496,30 +532,32 @@ 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 style = theme.style(&self.class, status); + renderer.fill_quad( renderer::Quad { bounds, - border: appearance.border, + border: style.border, ..renderer::Quad::default() }, - appearance.background, + style.background, ); renderer.fill_editor( &internal.editor, bounds.position() + Vector::new(self.padding.left, self.padding.top), - style.text_color, + defaults.text_color, *viewport, ); @@ -531,27 +569,31 @@ where if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - let position = position + translation; + let cursor = + Rectangle::new( + position + translation, + Size::new( + 1.0, + self.line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + ), + ); - if bounds.contains(position) { + if let Some(clipped_cursor) = bounds.intersection(&cursor) { renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: position.x, - y: position.y, - width: 1.0, - height: self - .line_height - .to_absolute( - self.text_size.unwrap_or_else( - || renderer.default_size(), - ), - ) - .into(), + x: clipped_cursor.x.floor(), + y: clipped_cursor.y, + width: clipped_cursor.width, + height: clipped_cursor.height, }, ..renderer::Quad::default() }, - theme.value_color(&self.style), + style.value, ); } } @@ -564,7 +606,7 @@ where bounds: range, ..renderer::Quad::default() }, - theme.selection_color(&self.style), + style.selection, ); } } @@ -600,7 +642,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Message: 'a, - Theme: StyleSheet + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer, { fn from( @@ -776,3 +818,101 @@ 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 Style { + /// 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 theme catalog of a [`TextEditor`]. +pub trait Catalog { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`TextEditor`]. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`TextEditor`]. +pub fn default(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + + let active = Style { + 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 => Style { + border: Border { + color: palette.background.base.text, + ..active.border + }, + ..active + }, + Status::Focused => Style { + border: Border { + color: palette.primary.strong.color, + ..active.border + }, + ..active + }, + Status::Disabled => Style { + 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 92c4892c..e9f07838 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -27,19 +27,16 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - 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 { @@ -63,7 +60,7 @@ pub struct TextInput< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { id: Option<Id>, @@ -79,7 +76,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, + class: Theme::Class<'a>, } /// The default [`Padding`] of a [`TextInput`]. @@ -88,14 +85,11 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0); impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value + /// Creates a new [`TextInput`] with the given placeholder and + /// its current value. pub fn new(placeholder: &str, value: &str) -> Self { TextInput { id: None, @@ -111,7 +105,7 @@ where on_paste: None, on_submit: None, icon: None, - style: Default::default(), + class: Theme::default(), } } @@ -198,8 +192,19 @@ where } /// Sets the style of the [`TextInput`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`TextInput`]. + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } @@ -213,20 +218,92 @@ 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.as_str(), + 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 mut content = [0; 4]; + + let icon_text = Text { + line_height: self.line_height, + content: icon.code_point.encode_utf8(&mut content) as &_, + 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 @@ -243,19 +320,175 @@ 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 style = theme.style(&self.class, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: style.border, + ..renderer::Quad::default() + }, + style.background, ); + + if self.icon.is_some() { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_paragraph( + &state.icon, + icon_layout.bounds().center(), + style.icon, + *viewport, + ); + } + + let text = value.to_string(); + + let (cursor, offset, is_selecting) = 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() + }, + style.value, + )) + } else { + None + }; + + (cursor, offset, false) + } + 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() + }, + style.selection, + )), + if end == right { + right_offset + } else { + left_offset + }, + true, + ) + } + } + } else { + (None, 0.0, false) + }; + + 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() { + style.placeholder + } else { + style.value + }, + viewport, + ); + }; + + if is_selecting { + renderer + .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); + } else { + draw(renderer, text_bounds); + } } } @@ -263,7 +496,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -299,20 +532,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( @@ -339,23 +559,470 @@ 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() + && !self.is_secure => + { + 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() + && !self.is_secure => + { + 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( @@ -368,19 +1035,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( @@ -391,15 +1046,23 @@ 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() + } } } impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where - Message: 'a + Clone, - Theme: StyleSheet + 'a, + Message: Clone + 'a, + Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -488,767 +1151,6 @@ 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); - - 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::Character("c") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard.write( - clipboard::Kind::Standard, - 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(value) - { - clipboard.write( - clipboard::Kind::Standard, - 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); - - 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(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); - - return event::Status::Captured; - } - keyboard::Key::Character("a") - if state.keyboard_modifiers.command() => - { - state.cursor.select_all(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(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; - } - } - - 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::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(); - - 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(); - - 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> { @@ -1264,6 +1166,12 @@ pub struct State<P: text::Paragraph> { // 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 { updated_at: Instant, @@ -1479,3 +1387,103 @@ 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 Style { + /// 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 theme catalog of a [`TextInput`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`TextInput`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`TextInput`]. +pub fn default(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + + let active = Style { + 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 => Style { + border: Border { + color: palette.background.base.text, + ..active.border + }, + ..active + }, + Status::Focused => Style { + border: Border { + color: palette.primary.strong.color, + ..active.border + }, + ..active + }, + Status::Disabled => Style { + background: Background::Color(palette.background.weak.color), + value: active.placeholder, + ..active + }, + } +} diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 3a5fd823..f4597458 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -7,58 +7,68 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, - Size, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Size, Vector, Widget, }; -use crate::style::application; + +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, - Theme: application::StyleSheet, { - content: Element<'a, Message, Theme, Renderer>, - theme: Theme, - style: Theme::Style, - show_background: bool, + 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, - Theme: application::StyleSheet, { /// 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 { content: content.into(), - theme, - style: Theme::Style::default(), - show_background: false, + to_theme, + text_color: None, + background: None, + old_theme: PhantomData, } } - /// Sets whether to draw the background color of the `Theme`. - pub fn background(mut self, background: bool) -> Self { - self.show_background = background; + /// 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, - Theme: application::StyleSheet, { fn tag(&self) -> tree::Tag { self.content.as_widget().tag() @@ -134,38 +144,36 @@ where &self, tree: &Tree, renderer: &mut Renderer, - _theme: &AnyTheme, - _style: &renderer::Style, + theme: &Theme, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = self.theme.appearance(&self.style); + let theme = (self.to_theme)(theme); - if self.show_background { + if let Some(background) = self.background { container::draw_background( renderer, - &container::Appearance { - background: Some(Background::Color( - appearance.background_color, - )), - ..container::Appearance::default() + &container::Style { + background: Some(background(&theme)), + ..container::Style::default() }, layout.bounds(), ); } - self.content.as_widget().draw( - tree, - renderer, - &self.theme, - &renderer::Style { - text_color: appearance.text_color, - }, - layout, - cursor, - viewport, - ); + 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>( @@ -174,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, { @@ -197,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( @@ -252,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))) @@ -268,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 + application::StyleSheet, + 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 4e3925ba..ca6e37b0 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,7 @@ pub struct Toggler< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { is_toggled: bool, @@ -52,16 +49,16 @@ pub struct Toggler< text_shaping: text::Shaping, spacing: f32, font: Option<Renderer::Font>, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet, + Theme: Catalog, 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`]. /// @@ -91,7 +88,7 @@ where text_shaping: text::Shaping::Basic, spacing: Self::DEFAULT_SIZE / 2.0, font: None, - style: Default::default(), + class: Theme::default(), } } @@ -149,8 +146,20 @@ where } /// Sets the style of the [`Toggler`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Toggler`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -158,7 +167,7 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet + crate::text::StyleSheet, + Theme: Catalog, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -286,7 +295,7 @@ where style, label_layout, tree.state.downcast_ref(), - crate::text::Appearance::default(), + crate::text::Style::default(), viewport, ); } @@ -294,12 +303,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 style = theme.style(&self.class, status); + let border_radius = bounds.height / BORDER_RADIUS_RATIO; let space = SPACE_RATIO * bounds.height; @@ -354,7 +369,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: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -363,3 +378,108 @@ 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 Style { + /// 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 theme catalog of a [`Toggler`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style; +} + +/// A styling function for a [`Toggler`]. +/// +/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. +pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; + +impl Catalog for Theme { + type Class<'a> = StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(default) + } + + fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { + class(self, status) + } +} + +/// The default style of a [`Toggler`]. +pub fn default(theme: &Theme, status: Status) -> Style { + 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 + } + } + }; + + Style { + 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 d8a1e131..39f2e07d 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -20,7 +20,7 @@ pub struct Tooltip< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet + crate::text::StyleSheet, + Theme: container::Catalog, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -29,12 +29,12 @@ pub struct Tooltip< gap: f32, padding: f32, snap_within_viewport: bool, - style: <Theme as container::StyleSheet>::Style, + class: Theme::Class<'a>, } impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, + Theme: container::Catalog, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. @@ -55,7 +55,7 @@ where gap: 0.0, padding: Self::DEFAULT_PADDING, snap_within_viewport: true, - style: Default::default(), + class: Theme::default(), } } @@ -78,11 +78,23 @@ where } /// Sets the style of the [`Tooltip`]. + #[must_use] pub fn style( mut self, - style: impl Into<<Theme as container::StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); + style: impl Fn(&Theme) -> container::Style + 'a, + ) -> Self + where + Theme::Class<'a>: From<container::StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`Tooltip`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); self } } @@ -90,7 +102,7 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, + Theme: container::Catalog, Renderer: text::Renderer, { fn children(&self) -> Vec<widget::Tree> { @@ -239,7 +251,7 @@ where positioning: self.position, gap: self.gap, padding: self.padding, - style: &self.style, + class: &self.class, }))) } else { None @@ -262,7 +274,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: container::Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( @@ -273,11 +285,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, @@ -285,6 +296,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)] @@ -298,7 +311,7 @@ enum State { struct Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: container::StyleSheet + widget::text::StyleSheet, + Theme: container::Catalog, Renderer: text::Renderer, { position: Point, @@ -310,14 +323,14 @@ where positioning: Position, gap: f32, padding: f32, - style: &'b <Theme as container::StyleSheet>::Style, + class: &'b Theme::Class<'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, + Theme: container::Catalog, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -426,7 +439,7 @@ where layout: Layout<'_>, cursor_position: mouse::Cursor, ) { - let style = container::StyleSheet::appearance(theme, self.style); + let style = theme.style(self.class); container::draw_background(renderer, &style, layout.bounds()); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 8f7c88da..defb442f 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -1,11 +1,10 @@ //! 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, Catalog, Handle, HandleShape, Status, Style, StyleFn, +}; -use crate::core; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -15,8 +14,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size, - Widget, + self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, + Size, Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -29,8 +28,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 { @@ -44,7 +42,7 @@ use crate::core::{ #[allow(missing_debug_implementations)] pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> where - Theme: StyleSheet, + Theme: Catalog, { range: RangeInclusive<T>, step: T, @@ -55,17 +53,17 @@ where on_release: Option<Message>, width: f32, height: Length, - style: Theme::Style, + class: Theme::Class<'a>, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: StyleSheet, + Theme: Catalog, { /// The default width of a [`VerticalSlider`]. - pub const DEFAULT_WIDTH: f32 = 22.0; + pub const DEFAULT_WIDTH: f32 = 16.0; /// Creates a new [`VerticalSlider`]. /// @@ -101,7 +99,7 @@ where on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, - style: Default::default(), + class: Theme::default(), } } @@ -136,12 +134,6 @@ where self } - /// Sets the style of the [`VerticalSlider`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); - self - } - /// Sets the step size of the [`VerticalSlider`]. pub fn step(mut self, step: T) -> Self { self.step = step; @@ -155,6 +147,24 @@ where self.shift_step = Some(shift_step.into()); self } + + /// Sets the style of the [`VerticalSlider`]. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the style class of the [`VerticalSlider`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -162,7 +172,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Theme: StyleSheet, + Theme: Catalog, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { @@ -170,7 +180,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -200,20 +210,146 @@ 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.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(); + + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); + + 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.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) = 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( @@ -226,15 +362,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, - theme, - &self.style, + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = theme.style( + &self.class, + 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 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() + }, + 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() + }, + 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() + }, + style.handle.color, ); } @@ -246,7 +459,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() + } } } @@ -256,7 +479,7 @@ impl<'a, T, Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: StyleSheet + 'a, + Theme: Catalog + 'a, Renderer: core::Renderer + 'a, { fn from( @@ -266,294 +489,8 @@ where } } -/// 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 locate = |cursor_position: Point| -> Option<T> { - let bounds = layout.bounds(); - - 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 { - 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(); - - let percent = 1.0 - - f64::from(cursor_position.y - bounds.y) - / f64::from(bounds.height); - - 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() { - 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; - } - }; - - 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() - { - let _ = 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); - } - 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 -} - -/// 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 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, - }, - 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, - }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, - }, - ..renderer::Quad::default() - }, - style.handle.color, - ); -} - -/// 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() - } -} - -/// 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 fca2919a..c06afeeb 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] system = ["sysinfo"] @@ -21,13 +24,15 @@ wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] multi-window = ["iced_runtime/multi-window"] [dependencies] +iced_futures.workspace = true iced_graphics.workspace = true iced_runtime.workspace = true -iced_style.workspace = true log.workspace = true +rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true +wasm-bindgen-futures.workspace = true window_clipboard.workspace = true winit.workspace = true @@ -40,3 +45,4 @@ winapi.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true web-sys.features = ["Document", "Window"] + diff --git a/winit/src/application.rs b/winit/src/application.rs index b19a5517..3d11bd4a 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -7,23 +7,26 @@ use crate::conversion; use crate::core; use crate::core::mouse; use crate::core::renderer; +use crate::core::theme; use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; -use crate::core::{Event, Point, Size}; +use crate::core::{Color, Event, Point, Size, Theme}; use crate::debug; use crate::futures::futures; use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics; use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Command; -use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; +use futures::channel::oneshot; +use std::borrow::Cow; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -40,7 +43,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; @@ -65,8 +68,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 @@ -96,34 +99,76 @@ 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; + + /// Returns a [`Palette`] for the [`Application`], if possible. + fn palette(&self) -> Option<theme::Palette> { + None + } +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } + + fn palette(&self) -> Option<theme::Palette> { + Some(self.palette()) + } +} + +/// 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>( settings: Settings<A::Flags>, - compositor_settings: C::Settings, + graphics_settings: graphics::Settings, ) -> Result<(), Error> 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; - use winit::event_loop::EventLoopBuilder; + use winit::event_loop::EventLoop; let boot_timer = debug::boot_time(); - let event_loop = EventLoopBuilder::with_user_event() + let event_loop = EventLoop::with_user_event() .build() .expect("Create event loop"); - let proxy = event_loop.create_proxy(); + + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); let runtime = { - let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); - Runtime::new(executor, proxy) + Runtime::new(executor, proxy.clone()) }; let (application, init_command) = { @@ -132,104 +177,282 @@ where runtime.enter(|| A::new(flags)) }; - #[cfg(target_arch = "wasm32")] - let target = settings.window.platform_specific.target.clone(); + let id = settings.id; + let title = application.title(); - let should_be_visible = settings.window.visible; - let exit_on_close_request = settings.window.exit_on_close_request; + let (boot_sender, boot_receiver) = oneshot::channel(); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); - let builder = conversion::window_settings( - settings.window, - &application.title(), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); - - log::debug!("Window builder: {builder:#?}"); - - let window = Arc::new( - builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?, - ); - - #[cfg(target_arch = "wasm32")] - { - 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(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{target}")) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node - .replace_with_with_node_1(&canvas) - .expect(&format!("Could not replace #{}", node.id())); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - } - - let compositor = C::new(compositor_settings, window.clone())?; - let mut renderer = compositor.create_renderer(); - - for font in settings.fonts { - use crate::core::text::Renderer; - - renderer.load_font(font); - } - - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::<A, E, C>( + let instance = Box::pin(run_instance::<A, E, C>( application, - compositor, - renderer, runtime, proxy, + boot_receiver, event_receiver, control_sender, init_command, - window, - should_be_visible, - exit_on_close_request, + settings.fonts, boot_timer, )); - let mut context = task::Context::from_waker(task::noop_waker_ref()); + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner<Message: 'static, F, C> { + instance: std::pin::Pin<Box<F>>, + context: task::Context<'static>, + boot: Option<BootConfig<C>>, + sender: mpsc::UnboundedSender<winit::event::Event<Message>>, + receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>, + error: Option<Error>, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc<std::cell::RefCell<bool>>, + #[cfg(target_arch = "wasm32")] + queued_events: Vec<winit::event::Event<Message>>, + } + + struct BootConfig<C> { + sender: oneshot::Sender<Boot<C>>, + id: Option<String>, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } + + let runner = Runner { + instance, + context, + boot: Some(BootConfig { + sender: boot_sender, + id, + title, + window_settings: settings.window, + graphics_settings, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), + #[cfg(target_arch = "wasm32")] + queued_events: Vec::new(), + }; + + impl<Message, F, C> winit::application::ApplicationHandler<Message> + for Runner<Message, F, C> + where + F: Future<Output = ()>, + C: Compositor + 'static, + { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let Some(BootConfig { + sender, + id, + title, + window_settings, + graphics_settings, + }) = self.boot.take() + else { + return; + }; + + let should_be_visible = window_settings.visible; + let exit_on_close_request = window_settings.exit_on_close_request; + + #[cfg(target_arch = "wasm32")] + let target = window_settings.platform_specific.target.clone(); + + let window_attributes = conversion::window_attributes( + window_settings, + &title, + event_loop.primary_monitor(), + id, + ) + .with_visible(false); + + log::debug!("Window attributes: {window_attributes:#?}"); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } + }; + + let finish_boot = { + let window = window.clone(); + + async move { + let compositor = + C::new(graphics_settings, window.clone()).await?; + + sender + .send(Boot { + window, + compositor, + should_be_visible, + exit_on_close_request, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + } + }; + + #[cfg(not(target_arch = "wasm32"))] + if let Err(error) = futures::executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + + #[cfg(target_arch = "wasm32")] + { + 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(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!("#{target}")) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node.replace_with_with_node_1(&canvas).expect( + &format!("Could not replace #{}", node.id()), + ); + } + None => { + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } + }; + + let is_booted = self.is_booted.clone(); + + wasm_bindgen_futures::spawn_local(async move { + finish_boot.await.expect("Finish boot!"); + + *is_booted.borrow_mut() = true; + }); + } + } + + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + winit::event::Event::NewEvents(cause), + ); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + winit::event::Event::WindowEvent { window_id, 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")] + { + if is_move_or_resize { + self.process_event( + event_loop, + winit::event::Event::AboutToWait, + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + winit::event::Event::UserEvent(message), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event(event_loop, winit::event::Event::AboutToWait); + } + } + + impl<Message, F, C> Runner<Message, F, C> + where + F: Future<Output = ()>, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: winit::event::Event<Message>, + ) { + // On Wasm, events may start being processed before the compositor + // boots up. We simply queue them and process them once ready. + #[cfg(target_arch = "wasm32")] + if !*self.is_booted.borrow() { + self.queued_events.push(event); + return; + } else if !self.queued_events.is_empty() { + let queued_events = std::mem::take(&mut self.queued_events); + + // This won't infinitely recurse, since we `mem::take` + for event in queued_events { + self.process_event(event_loop, event); + } + } - let process_event = - move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { if event_loop.exiting() { return; } - event_sender.start_send(event).expect("Send event"); + self.sender.start_send(event).expect("Send event"); - let poll = instance.as_mut().poll(&mut context); + let poll = self.instance.as_mut().poll(&mut self.context); match poll { task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { + if let Ok(Some(flow)) = self.receiver.try_next() { event_loop.set_control_flow(flow); } } @@ -237,64 +460,68 @@ where event_loop.exit(); } } - }; - - #[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(()) + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } +} + +struct Boot<C> { + window: Arc<winit::window::Window>, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, } async fn run_instance<A, E, C>( mut application: A, - mut compositor: C, - mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy<A::Message>, + mut proxy: Proxy<A::Message>, + mut boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event<A::Message>, >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, - window: Arc<winit::window::Window>, - should_be_visible: bool, - exit_on_close_request: bool, + fonts: Vec<Cow<'static, [u8]>>, boot_timer: debug::Timer, ) where 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; use winit::event_loop::ControlFlow; + let Boot { + window, + mut compositor, + should_be_visible, + exit_on_close_request, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut renderer = compositor.create_renderer(); + + for font in fonts { + compositor.load_font(font); + } + let mut state = State::new(&application, &window); let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); @@ -339,6 +566,7 @@ async fn run_instance<A, E, C>( let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); + let mut user_events = 0; let mut redraw_pending = false; while let Some(event) = event_receiver.next().await { @@ -363,6 +591,7 @@ async fn run_instance<A, E, C>( } event::Event::UserEvent(message) => { messages.push(message); + user_events += 1; } event::Event::WindowEvent { event: event::WindowEvent::RedrawRequested { .. }, @@ -444,7 +673,7 @@ async fn run_instance<A, E, C>( draw_timer.finish(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( + window.set_cursor(conversion::mouse_interaction( new_mouse_interaction, )); @@ -558,6 +787,11 @@ async fn run_instance<A, E, C>( if should_exit { break; } + + if user_events > 0 { + proxy.free_slots(user_events); + user_events = 0; + } } if !redraw_pending { @@ -606,7 +840,7 @@ pub fn build_user_interface<'a, A: Application>( size: Size, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { let view_timer = debug::view_time(window::Id::MAIN); let view = application.view(); @@ -631,12 +865,12 @@ pub fn update<A: Application, C, E: Executor>( runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + proxy: &mut Proxy<A::Message>, messages: &mut Vec<A::Message>, 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); @@ -679,13 +913,13 @@ pub fn run_command<A, C, E>( runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, should_exit: &mut bool, - proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + proxy: &mut Proxy<A::Message>, window: &winit::window::Window, ) where A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::command; use crate::runtime::system; @@ -703,9 +937,7 @@ pub fn run_command<A, C, E>( clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } clipboard::Action::Write(contents, kind) => { clipboard.write(kind, contents); @@ -735,25 +967,16 @@ pub fn run_command<A, C, E>( let size = window.inner_size().to_logical(window.scale_factor()); - proxy - .send_event(callback(Size::new( - size.width, - size.height, - ))) - .expect("Send message to event loop"); + proxy.send(callback(Size::new(size.width, size.height))); } window::Action::FetchMaximized(_id, callback) => { - proxy - .send_event(callback(window.is_maximized())) - .expect("Send message to event loop"); + proxy.send(callback(window.is_maximized())); } window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } window::Action::FetchMinimized(_id, callback) => { - proxy - .send_event(callback(window.is_minimized())) - .expect("Send message to event loop"); + proxy.send(callback(window.is_minimized())); } window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); @@ -769,9 +992,7 @@ pub fn run_command<A, C, E>( }) .ok(); - proxy - .send_event(callback(position)) - .expect("Send message to event loop"); + proxy.send(callback(position)); } window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { @@ -796,9 +1017,7 @@ pub fn run_command<A, C, E>( core::window::Mode::Hidden }; - proxy - .send_event(tag(mode)) - .expect("Send message to event loop"); + proxy.send(tag(mode)); } window::Action::ToggleMaximize(_id) => { window.set_maximized(!window.is_maximized()); @@ -826,17 +1045,13 @@ pub fn run_command<A, C, E>( } } window::Action::FetchId(_id, tag) => { - proxy - .send_event(tag(window.id().into())) - .expect("Send message to event loop"); + proxy.send(tag(window.id().into())); } window::Action::RunWithHandle(_id, tag) => { use window::raw_window_handle::HasWindowHandle; if let Ok(handle) = window.window_handle() { - proxy - .send_event(tag(&handle)) - .expect("Send message to event loop"); + proxy.send(tag(handle)); } } @@ -848,12 +1063,10 @@ pub fn run_command<A, C, E>( state.background_color(), ); - proxy - .send_event(tag(window::Screenshot::new( - bytes, - state.physical_size(), - ))) - .expect("Send message to event loop."); + proxy.send(tag(window::Screenshot::new( + bytes, + state.physical_size(), + ))); } }, command::Action::System(action) => match action { @@ -861,7 +1074,7 @@ pub fn run_command<A, C, E>( #[cfg(feature = "system")] { let graphics_info = compositor.fetch_information(); - let proxy = proxy.clone(); + let mut proxy = proxy.clone(); let _ = std::thread::spawn(move || { let information = @@ -869,9 +1082,7 @@ pub fn run_command<A, C, E>( let message = _tag(information); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); }); } } @@ -893,9 +1104,7 @@ pub fn run_command<A, C, E>( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); @@ -907,14 +1116,10 @@ pub fn run_command<A, C, E>( *cache = current_cache; } command::Action::LoadFont { bytes, tagger } => { - use crate::core::text::Renderer; - // TODO: Error handling (?) - renderer.load_font(bytes); + compositor.load_font(bytes); - proxy - .send_event(tagger(Ok(()))) - .expect("Send message to event loop"); + proxy.send(tagger(Ok(()))); } command::Action::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index b70d8dc2..eae9c3a8 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,16 +29,17 @@ 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 _ = theme.palette().map(debug::theme_changed); + let _ = application::DefaultStyle::palette(&theme) + .map(debug::theme_changed); let viewport = { let physical_size = window.inner_size(); @@ -213,8 +214,9 @@ where // Update theme and appearance self.theme = application.theme(); - self.appearance = self.theme.appearance(&application.style()); + self.appearance = application.style(&self.theme); - let _ = self.theme.palette().map(debug::theme_changed); + let _ = application::DefaultStyle::palette(&self.theme) + .map(debug::theme_changed); } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index fc3d1c08..d04fc2f1 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -8,16 +8,16 @@ use crate::core::touch; use crate::core::window; use crate::core::{Event, Point, Size}; -/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`. -pub fn window_settings( +/// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`. +pub fn window_attributes( settings: window::Settings, title: &str, primary_monitor: Option<winit::monitor::MonitorHandle>, _id: Option<String>, -) -> winit::window::WindowBuilder { - let mut window_builder = winit::window::WindowBuilder::new(); +) -> winit::window::WindowAttributes { + let mut attributes = winit::window::WindowAttributes::default(); - window_builder = window_builder + attributes = attributes .with_title(title) .with_inner_size(winit::dpi::LogicalSize { width: settings.size.width, @@ -39,23 +39,21 @@ pub fn window_settings( if let Some(position) = position(primary_monitor.as_ref(), settings.size, settings.position) { - window_builder = window_builder.with_position(position); + attributes = attributes.with_position(position); } if let Some(min_size) = settings.min_size { - window_builder = - window_builder.with_min_inner_size(winit::dpi::LogicalSize { - width: min_size.width, - height: min_size.height, - }); + attributes = attributes.with_min_inner_size(winit::dpi::LogicalSize { + width: min_size.width, + height: min_size.height, + }); } if let Some(max_size) = settings.max_size { - window_builder = - window_builder.with_max_inner_size(winit::dpi::LogicalSize { - width: max_size.width, - height: max_size.height, - }); + attributes = attributes.with_max_inner_size(winit::dpi::LogicalSize { + width: max_size.width, + height: max_size.height, + }); } #[cfg(any( @@ -65,35 +63,33 @@ pub fn window_settings( target_os = "openbsd" ))] { - // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do - // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. - use ::winit::platform::wayland::WindowBuilderExtWayland; + use ::winit::platform::wayland::WindowAttributesExtWayland; if let Some(id) = _id { - window_builder = window_builder.with_name(id.clone(), id); + attributes = attributes.with_name(id.clone(), id); } } #[cfg(target_os = "windows")] { - use winit::platform::windows::WindowBuilderExtWindows; + use winit::platform::windows::WindowAttributesExtWindows; #[allow(unsafe_code)] unsafe { - window_builder = window_builder + attributes = attributes .with_parent_window(settings.platform_specific.parent); } - window_builder = window_builder + attributes = attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); - window_builder = window_builder + attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); } #[cfg(target_os = "macos")] { - use winit::platform::macos::WindowBuilderExtMacOS; + use winit::platform::macos::WindowAttributesExtMacOS; - window_builder = window_builder + attributes = attributes .with_title_hidden(settings.platform_specific.title_hidden) .with_titlebar_transparent( settings.platform_specific.titlebar_transparent, @@ -107,25 +103,25 @@ pub fn window_settings( { #[cfg(feature = "x11")] { - use winit::platform::x11::WindowBuilderExtX11; + use winit::platform::x11::WindowAttributesExtX11; - window_builder = window_builder.with_name( + attributes = attributes.with_name( &settings.platform_specific.application_id, &settings.platform_specific.application_id, ); } #[cfg(feature = "wayland")] { - use winit::platform::wayland::WindowBuilderExtWayland; + use winit::platform::wayland::WindowAttributesExtWayland; - window_builder = window_builder.with_name( + attributes = attributes.with_name( &settings.platform_specific.application_id, &settings.platform_specific.application_id, ); } } - window_builder + attributes } /// Converts a winit window event into an iced event. @@ -396,7 +392,9 @@ pub fn mouse_interaction( use mouse::Interaction; match interaction { - Interaction::Idle => winit::window::CursorIcon::Default, + Interaction::None | Interaction::Idle => { + winit::window::CursorIcon::Default + } Interaction::Pointer => winit::window::CursorIcon::Pointer, Interaction::Working => winit::window::CursorIcon::Progress, Interaction::Grab => winit::window::CursorIcon::Grab, diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 5936ded3..e6d58152 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -17,21 +17,12 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - unsafe_code, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use iced_graphics as graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::debug; 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 7f361e0e..61c0f736 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -13,17 +13,22 @@ use crate::core::window; use crate::core::{Point, Size}; use crate::debug; use crate::futures::futures::channel::mpsc; -use crate::futures::futures::{task, Future, StreamExt}; +use crate::futures::futures::channel::oneshot; +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; use crate::graphics::{compositor, Compositor}; use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; -use std::collections::HashMap; +pub use crate::application::{default, Appearance, DefaultStyle}; + +use rustc_hash::FxHashMap; use std::mem::ManuallyDrop; use std::sync::Arc; use std::time::Instant; @@ -41,7 +46,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; @@ -66,8 +71,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 @@ -102,29 +107,29 @@ where /// settings. pub fn run<A, E, C>( settings: Settings<A::Flags>, - compositor_settings: C::Settings, + graphics_settings: graphics::Settings, ) -> Result<(), Error> where A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { - use winit::event_loop::EventLoopBuilder; + use winit::event_loop::EventLoop; let boot_timer = debug::boot_time(); - let event_loop = EventLoopBuilder::with_user_event() + let event_loop = EventLoop::with_user_event() .build() .expect("Create event loop"); - let proxy = event_loop.create_proxy(); + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); let runtime = { - let proxy = Proxy::new(event_loop.create_proxy()); let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); - Runtime::new(executor, proxy) + Runtime::new(executor, proxy.clone()) }; let (application, init_command) = { @@ -133,186 +138,292 @@ where runtime.enter(|| A::new(flags)) }; - let should_main_be_visible = settings.window.visible; - let exit_on_close_request = settings.window.exit_on_close_request; + let id = settings.id; + let title = application.title(window::Id::MAIN); - let builder = conversion::window_settings( - settings.window, - &application.title(window::Id::MAIN), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); + let (boot_sender, boot_receiver) = oneshot::channel(); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); - log::info!("Window builder: {:#?}", builder); - - let main_window = Arc::new( - builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?, - ); - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = main_window.canvas(); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{}", target)) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node - .replace_with_with_node_1(&canvas) - .expect(&format!("Could not replace #{}", node.id())); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - } - - let mut compositor = C::new(compositor_settings, main_window.clone())?; - - let mut window_manager = WindowManager::new(); - let _ = window_manager.insert( - window::Id::MAIN, - main_window, - &application, - &mut compositor, - exit_on_close_request, - ); - - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin(run_instance::<A, E, C>( + let instance = Box::pin(run_instance::<A, E, C>( application, - compositor, runtime, proxy, + boot_receiver, event_receiver, control_sender, init_command, - window_manager, - should_main_be_visible, boot_timer, )); - let mut context = task::Context::from_waker(task::noop_waker_ref()); + let context = task::Context::from_waker(task::noop_waker_ref()); - let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| { - if event_loop.exiting() { - return; - } - - event_sender - .start_send(Event::EventLoopAwakened(event)) - .expect("Send event"); - - loop { - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => match control_receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if new < current => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); - } - } - } - Control::CreateWindow { - id, - settings, - title, - monitor, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let window = conversion::window_settings( - settings, &title, monitor, None, - ) - .build(event_loop) - .expect("Failed to build window"); - - event_sender - .start_send(Event::WindowCreated { - id, - window, - exit_on_close_request, - }) - .expect("Send event"); - } - Control::Exit => { - event_loop.exit(); - } - }, - _ => { - break; - } - }, - task::Poll::Ready(_) => { - event_loop.exit(); - break; - } - }; - } - }; - - #[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); - } - }); + struct Runner<Message: 'static, F, C> { + instance: std::pin::Pin<Box<F>>, + context: task::Context<'static>, + boot: Option<BootConfig<C>>, + sender: mpsc::UnboundedSender<Event<Message>>, + receiver: mpsc::UnboundedReceiver<Control>, + error: Option<Error>, } + struct BootConfig<C> { + sender: oneshot::Sender<Boot<C>>, + id: Option<String>, + title: String, + window_settings: window::Settings, + graphics_settings: graphics::Settings, + } + + let mut runner = Runner { + instance, + context, + boot: Some(BootConfig { + sender: boot_sender, + id, + title, + window_settings: settings.window, + graphics_settings, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + }; + + impl<Message, F, C> winit::application::ApplicationHandler<Message> + for Runner<Message, F, C> + where + F: Future<Output = ()>, + C: Compositor, + { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let Some(BootConfig { + sender, + id, + title, + window_settings, + graphics_settings, + }) = self.boot.take() + else { + return; + }; + + let should_be_visible = window_settings.visible; + let exit_on_close_request = window_settings.exit_on_close_request; + + let window_attributes = conversion::window_attributes( + window_settings, + &title, + event_loop.primary_monitor(), + id, + ) + .with_visible(false); + + log::debug!("Window attributes: {window_attributes:#?}"); + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } + }; + + let finish_boot = async move { + let compositor = + C::new(graphics_settings, window.clone()).await?; + + sender + .send(Boot { + window, + compositor, + should_be_visible, + exit_on_close_request, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + }; + + if let Err(error) = executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + } + + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::WindowEvent { + window_id, + 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")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + message, + )), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl<Message, F, C> Runner<Message, F, C> + where + F: Future<Output = ()>, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event<Message>, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = event_loop + .create_window( + conversion::window_attributes( + settings, &title, monitor, None, + ), + ) + .expect("Create window"); + + self.process_event( + event_loop, + Event::WindowCreated { + id, + window, + exit_on_close_request, + }, + ); + } + Control::Exit => { + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + let _ = event_loop.run_app(&mut runner); + Ok(()) } +struct Boot<C> { + window: Arc<winit::window::Window>, + compositor: C, + should_be_visible: bool, + exit_on_close_request: bool, +} + enum Event<Message: 'static> { WindowCreated { id: window::Id, @@ -335,29 +446,44 @@ enum Control { async fn run_instance<A, E, C>( mut application: A, - mut compositor: C, mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy<A::Message>, + mut proxy: Proxy<A::Message>, + mut boot: oneshot::Receiver<Boot<C>>, mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, mut control_sender: mpsc::UnboundedSender<Control>, init_command: Command<A::Message>, - mut window_manager: WindowManager<A, C>, - should_main_window_be_visible: bool, boot_timer: debug::Timer, ) where 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; + let Boot { + window: main_window, + mut compositor, + should_be_visible, + exit_on_close_request, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut window_manager = WindowManager::new(); + + let _ = window_manager.insert( + window::Id::MAIN, + main_window.clone(), + &application, + &mut compositor, + exit_on_close_request, + ); + let main_window = window_manager .get_mut(window::Id::MAIN) .expect("Get main window"); - if should_main_window_be_visible { + if should_be_visible { main_window.raw.set_visible(true); } @@ -375,7 +501,15 @@ async fn run_instance<A, E, C>( )] }; - let mut ui_caches = HashMap::new(); + let mut ui_caches = FxHashMap::default(); + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut window_manager, + FxHashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), + )); run_command( &application, @@ -392,16 +526,8 @@ async fn run_instance<A, E, C>( runtime.track(application.subscription().into_recipes()); boot_timer.finish(); - let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut window_manager, - HashMap::from_iter([( - window::Id::MAIN, - user_interface::Cache::default(), - )]), - )); - let mut messages = Vec::new(); + let mut user_events = 0; 'main: while let Some(event) = event_receiver.next().await { match event { @@ -473,6 +599,7 @@ async fn run_instance<A, E, C>( } event::Event::UserEvent(message) => { messages.push(message); + user_events += 1; } event::Event::WindowEvent { window_id: id, @@ -521,7 +648,7 @@ async fn run_instance<A, E, C>( draw_timer.finish(); if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon( + window.raw.set_cursor( conversion::mouse_interaction( new_mouse_interaction, ), @@ -592,7 +719,7 @@ async fn run_instance<A, E, C>( if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon( + window.raw.set_cursor( conversion::mouse_interaction( new_mouse_interaction, ), @@ -742,7 +869,7 @@ async fn run_instance<A, E, C>( // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { - let mut cached_interfaces: HashMap< + let mut cached_interfaces: FxHashMap< window::Id, user_interface::Cache, > = ManuallyDrop::into_inner(user_interfaces) @@ -784,6 +911,11 @@ async fn run_instance<A, E, C>( &mut window_manager, cached_interfaces, )); + + if user_events > 0 { + proxy.free_slots(user_events); + user_events = 0; + } } } _ => {} @@ -804,7 +936,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, { let view_timer = debug::view_time(id); let view = application.view(id); @@ -825,13 +957,13 @@ fn update<A: Application, C, E: Executor>( runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + proxy: &mut Proxy<A::Message>, messages: &mut Vec<A::Message>, window_manager: &mut WindowManager<A, C>, - ui_caches: &mut HashMap<window::Id, user_interface::Cache>, + ui_caches: &mut FxHashMap<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); @@ -865,14 +997,14 @@ fn run_command<A, C, E>( runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + proxy: &mut Proxy<A::Message>, window_manager: &mut WindowManager<A, C>, - ui_caches: &mut HashMap<window::Id, user_interface::Cache>, + ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, ) where A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::clipboard; use crate::runtime::system; @@ -890,9 +1022,7 @@ fn run_command<A, C, E>( clipboard::Action::Read(tag, kind) => { let message = tag(clipboard.read(kind)); - proxy - .send_event(message) - .expect("Send message to event loop"); + proxy.send(message); } clipboard::Action::Write(contents, kind) => { clipboard.write(kind, contents); @@ -944,18 +1074,12 @@ fn run_command<A, C, E>( .to_logical(window.raw.scale_factor()); proxy - .send_event(callback(Size::new( - size.width, - size.height, - ))) - .expect("Send message to event loop"); + .send(callback(Size::new(size.width, size.height))); } } window::Action::FetchMaximized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(callback(window.raw.is_maximized())) - .expect("Send message to event loop"); + proxy.send(callback(window.raw.is_maximized())); } } window::Action::Maximize(id, maximized) => { @@ -965,9 +1089,7 @@ fn run_command<A, C, E>( } window::Action::FetchMinimized(id, callback) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(callback(window.raw.is_minimized())) - .expect("Send message to event loop"); + proxy.send(callback(window.raw.is_minimized())); } } window::Action::Minimize(id, minimized) => { @@ -989,9 +1111,7 @@ fn run_command<A, C, E>( }) .ok(); - proxy - .send_event(callback(position)) - .expect("Send message to event loop"); + proxy.send(callback(position)); } } window::Action::Move(id, position) => { @@ -1026,9 +1146,7 @@ fn run_command<A, C, E>( core::window::Mode::Hidden }; - proxy - .send_event(tag(mode)) - .expect("Send message to event loop"); + proxy.send(tag(mode)); } } window::Action::ToggleMaximize(id) => { @@ -1076,9 +1194,7 @@ fn run_command<A, C, E>( } window::Action::FetchId(id, tag) => { if let Some(window) = window_manager.get_mut(id) { - proxy - .send_event(tag(window.raw.id().into())) - .expect("Send message to event loop"); + proxy.send(tag(window.raw.id().into())); } } window::Action::RunWithHandle(id, tag) => { @@ -1088,9 +1204,7 @@ fn run_command<A, C, E>( .get_mut(id) .and_then(|window| window.raw.window_handle().ok()) { - proxy - .send_event(tag(&handle)) - .expect("Send message to event loop"); + proxy.send(tag(handle)); } } window::Action::Screenshot(id, tag) => { @@ -1102,12 +1216,10 @@ fn run_command<A, C, E>( window.state.background_color(), ); - proxy - .send_event(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), - ))) - .expect("Event loop doesn't exist."); + proxy.send(tag(window::Screenshot::new( + bytes, + window.state.physical_size(), + ))); } } }, @@ -1116,7 +1228,7 @@ fn run_command<A, C, E>( #[cfg(feature = "system")] { let graphics_info = compositor.fetch_information(); - let proxy = proxy.clone(); + let mut proxy = proxy.clone(); let _ = std::thread::spawn(move || { let information = @@ -1124,9 +1236,7 @@ fn run_command<A, C, E>( let message = _tag(information); - proxy - .send_event(message) - .expect("Event loop doesn't exist."); + proxy.send(message); }); } } @@ -1150,9 +1260,7 @@ fn run_command<A, C, E>( match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Event loop doesn't exist."); + proxy.send(message); // operation completed, don't need to try to operate on rest of UIs break 'operate; @@ -1169,17 +1277,10 @@ fn run_command<A, C, E>( uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); } command::Action::LoadFont { bytes, tagger } => { - use crate::core::text::Renderer; - - // TODO change this once we change each renderer to having a single backend reference.. :pain: // TODO: Error handling (?) - for (_, window) in window_manager.iter_mut() { - window.renderer.load_font(bytes.clone()); - } + compositor.load_font(bytes.clone()); - proxy - .send_event(tagger(Ok(()))) - .expect("Send message to event loop"); + proxy.send(tagger(Ok(()))); } command::Action::Custom(_) => { log::warn!("Unsupported custom action in `iced_winit` shell"); @@ -1189,14 +1290,14 @@ fn run_command<A, C, E>( } /// Build the user interface for every window. -pub fn build_user_interfaces<'a, A: Application, C: Compositor>( +pub fn build_user_interfaces<'a, A: Application, C>( application: &'a A, window_manager: &mut WindowManager<A, C>, - mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, -) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> + mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, +) -> FxHashMap<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 aeada137..bc88c04b 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(); @@ -231,6 +229,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 23f3c0ba..57a7dc7e 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -2,18 +2,18 @@ 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; use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] -pub struct WindowManager<A: Application, C: Compositor> +pub struct WindowManager<A, C> where - A::Theme: StyleSheet, + A: Application, C: Compositor<Renderer = A::Renderer>, + A::Theme: DefaultStyle, { aliases: BTreeMap<winit::window::WindowId, Id>, entries: BTreeMap<Id, Window<A, C>>, @@ -23,7 +23,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 { @@ -61,7 +61,7 @@ where exit_on_close_request, surface, renderer, - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, }, ); @@ -109,7 +109,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 +121,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 +136,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 diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 1d6c48bb..3edc30ad 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,28 +1,94 @@ use crate::futures::futures::{ channel::mpsc, + select, task::{Context, Poll}, - Sink, + Future, Sink, StreamExt, }; use std::pin::Pin; -/// An event loop proxy that implements `Sink`. +/// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] pub struct Proxy<Message: 'static> { raw: winit::event_loop::EventLoopProxy<Message>, + sender: mpsc::Sender<Message>, + notifier: mpsc::Sender<usize>, } impl<Message: 'static> Clone for Proxy<Message> { fn clone(&self) -> Self { Self { raw: self.raw.clone(), + sender: self.sender.clone(), + notifier: self.notifier.clone(), } } } impl<Message: 'static> Proxy<Message> { + const MAX_SIZE: usize = 100; + /// Creates a new [`Proxy`] from an `EventLoopProxy`. - pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self { - Self { raw } + pub fn new( + raw: winit::event_loop::EventLoopProxy<Message>, + ) -> (Self, impl Future<Output = ()>) { + let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); + let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); + let proxy = raw.clone(); + + let worker = async move { + let mut count = 0; + + loop { + if count < Self::MAX_SIZE { + select! { + message = receiver.select_next_some() => { + let _ = proxy.send_event(message); + count += 1; + + } + amount = processed.select_next_some() => { + count = count.saturating_sub(amount); + } + complete => break, + } + } else { + select! { + amount = processed.select_next_some() => { + count = count.saturating_sub(amount); + } + complete => break, + } + } + } + }; + + ( + Self { + raw, + sender, + notifier, + }, + worker, + ) + } + + /// Sends a `Message` to the event loop. + /// + /// Note: This skips the backpressure mechanism with an unbounded + /// channel. Use sparingly! + pub fn send(&mut self, message: Message) + where + Message: std::fmt::Debug, + { + self.raw + .send_event(message) + .expect("Send message to event loop"); + } + + /// Frees an amount of slots for additional messages to be queued in + /// this [`Proxy`]. + pub fn free_slots(&mut self, amount: usize) { + let _ = self.notifier.start_send(amount); } } @@ -30,32 +96,37 @@ impl<Message: 'static> Sink<Message> for Proxy<Message> { type Error = mpsc::SendError; fn poll_ready( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>> { - Poll::Ready(Ok(())) + self.sender.poll_ready(cx) } fn start_send( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, message: Message, ) -> Result<(), Self::Error> { - let _ = self.raw.send_event(message); - - Ok(()) + self.sender.start_send(message) } fn poll_flush( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>> { - Poll::Ready(Ok(())) + match self.sender.poll_ready(cx) { + Poll::Ready(Err(ref e)) if e.is_disconnected() => { + // If the receiver disconnected, we consider the sink to be flushed. + Poll::Ready(Ok(())) + } + x => x, + } } fn poll_close( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, _cx: &mut Context<'_>, ) -> Poll<Result<(), Self::Error>> { + self.sender.disconnect(); Poll::Ready(Ok(())) } }